diff --git a/.config/configuration.vsEnterprise.winget b/.config/configuration.vsEnterprise.winget
index 84e05ed511..4d19d37137 100644
--- a/.config/configuration.vsEnterprise.winget
+++ b/.config/configuration.vsEnterprise.winget
@@ -6,12 +6,16 @@ properties:
directives:
description: Enable Developer Mode
allowPrerelease: true
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
Ensure: Present
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2022 Enterprise (Any edition will work)
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
id: Microsoft.VisualStudio.2022.Enterprise
source: winget
@@ -21,6 +25,8 @@ properties:
directives:
description: Install required VS workloads
allowPrerelease: true
+ # Requires elevation for the get and set operations
+ securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Enterprise
channelId: VisualStudio.17.Release
diff --git a/.config/configuration.vsProfessional.winget b/.config/configuration.vsProfessional.winget
index 6ac0babf9f..78fb1c13d5 100644
--- a/.config/configuration.vsProfessional.winget
+++ b/.config/configuration.vsProfessional.winget
@@ -6,12 +6,16 @@ properties:
directives:
description: Enable Developer Mode
allowPrerelease: true
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
Ensure: Present
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2022 Professional (Any edition will work)
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
id: Microsoft.VisualStudio.2022.Professional
source: winget
@@ -21,6 +25,8 @@ properties:
directives:
description: Install required VS workloads
allowPrerelease: true
+ # Requires elevation for the get and set operations
+ securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Professional
channelId: VisualStudio.17.Release
diff --git a/.config/configuration.winget b/.config/configuration.winget
index df3eeea441..456eca47f2 100644
--- a/.config/configuration.winget
+++ b/.config/configuration.winget
@@ -6,12 +6,16 @@ properties:
directives:
description: Enable Developer Mode
allowPrerelease: true
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
Ensure: Present
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2022 Community (Any edition will work)
+ # Requires elevation for the set operation
+ securityContext: elevated
settings:
id: Microsoft.VisualStudio.2022.Community
source: winget
@@ -21,6 +25,8 @@ properties:
directives:
description: Install required VS workloads
allowPrerelease: true
+ # Requires elevation for the get and set operations
+ securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Community
channelId: VisualStudio.17.Release
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 08bbc04c55..e55d081c91 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,6 +1,5 @@
name: "🕷️ Bug report"
description: Report errors or unexpected behavior
-type: Bug
labels:
- Issue-Bug
- Needs-Triage
@@ -8,7 +7,8 @@ body:
- type: markdown
attributes:
value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one!
-- type: input
+- id: version
+ type: input
attributes:
label: Microsoft PowerToys version
placeholder: X.XX.X
@@ -16,7 +16,8 @@ body:
validations:
required: true
-- type: dropdown
+- id: installed
+ type: dropdown
attributes:
label: Installation method
description: How / Where was PowerToys installed from?
@@ -33,14 +34,6 @@ body:
validations:
required: true
-- type: dropdown
- attributes:
- label: Running as admin
- description: Are you running PowerToys as Admin?
- options:
- - "Yes"
- - "No"
-
- type: dropdown
attributes:
label: Area(s) with issue?
@@ -67,7 +60,7 @@ body:
- Keyboard Manager
- Mouse Utilities
- Mouse Without Borders
- - New+
+ - New+
- Peek
- PowerRename
- PowerToys Run
@@ -106,6 +99,19 @@ body:
validations:
required: false
+- id: additionalInfo
+ type: textarea
+ attributes:
+ label: Additional Information
+ placeholder: |
+ OS version
+ .Net version
+ System Language
+ User or System Installation
+ Running as admin
+ validations:
+ required: false
+
- type: textarea
attributes:
label: Other Software
@@ -116,3 +122,4 @@ body:
My Cool Application v0.3 (include a code snippet if it would help!)
validations:
required: false
+
diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt
index 619a036b32..10e9473258 100644
--- a/.github/actions/spell-check/allow/code.txt
+++ b/.github/actions/spell-check/allow/code.txt
@@ -273,4 +273,4 @@ mengyuanchen
testhost
#Tools
-OIP
+OIP
\ No newline at end of file
diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt
index 44131fecfb..eeda5b9ae1 100644
--- a/.github/actions/spell-check/allow/names.txt
+++ b/.github/actions/spell-check/allow/names.txt
@@ -188,6 +188,8 @@ zhaoqpcn
Zoltan
Zykova
Sameerjs
+ruslanlap
+vednig
# OTHERS
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index b688d6bae0..43447793a0 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -8,6 +8,7 @@ Acceleratorkeys
ACCEPTFILES
ACCESSDENIED
ACCESSTOKEN
+acfs
AClient
AColumn
acrt
@@ -56,12 +57,10 @@ APIIs
Apm
APPBARDATA
APPEXECLINK
-APPICONREFERENCE
APPLICATIONFRAMEHOST
appmanifest
APPMODEL
APPNAME
-APPPUBLISHER
appref
appsettings
appwindow
@@ -173,9 +172,9 @@ CCHFORMNAME
CCom
CContext
CDeclaration
-cdn
CElems
CENTERALIGN
+cer
certlm
certmgr
cfp
@@ -199,6 +198,7 @@ CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
closesocket
+clp
CLSCTX
clsids
Clusion
@@ -254,8 +254,6 @@ createdump
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
-CREATETHREAD
-CREATEWINDOW
CREATEWINDOWFAILED
CRECT
CRH
@@ -277,6 +275,7 @@ CURSORINFO
cursorpos
customaction
CUSTOMACTIONTEST
+CUSTOMFORMATPLACEHOLDER
CVal
cvd
CVirtual
@@ -308,11 +307,7 @@ DCOM
DComposition
DCR
ddd
-DDEAPPLICATION
-DDECOMMAND
DDEIf
-DDEIFEXEC
-DDETOPIC
DDevice
DDxgi
Deact
@@ -326,16 +321,13 @@ DEFAULTFLAGS
DEFAULTICON
defaultlib
DEFAULTONLY
-DEFAULTTOFOLDER
DEFAULTTONEAREST
DEFAULTTONULL
DEFAULTTOPRIMARY
-DEFAULTTOSTAR
DEFERERASE
DEFPUSHBUTTON
deinitialization
DELA
-DELEGATEEXECUTE
DELETEDKEYIMAGE
DELETESCANS
deletethis
@@ -355,6 +347,7 @@ DEVMODE
DEVMODEW
devpal
DIALOGEX
+digicert
dimm
DISABLEASACTIONKEY
DISABLENOSCROLL
@@ -389,7 +382,6 @@ dreamsofameaningfullife
drivedetectionwarning
Droid
DROPFILES
-DROPTARGET
DSTINVERT
DSurface
DTexture
@@ -510,12 +502,10 @@ Fira
FIXEDFILEINFO
FIXEDSYS
flac
-flaticon
flyouts
FMask
fmtid
FOF
-WANTNUKEWARNING
FOFX
FOLDERID
folderpath
@@ -525,17 +515,18 @@ FORCEMINIMIZE
FORMATDLGORD
formatetc
FORPARSING
-fpvm
Fqc
FRAMECHANGED
frm
Froml
FROMTOUCH
+fsanitize
fsmgmt
FZE
gacutil
Gaeilge
Gaidhlig
+gameid
GC'ed
GCLP
gdi
@@ -559,7 +550,6 @@ GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
GHND
-gifv
GMEM
GNumber
gpedit
@@ -643,6 +633,7 @@ HOTKEYF
hotkeys
hotlight
hotspot
+Hostx
HPAINTBUFFER
HRAWINPUT
HREDRAW
@@ -653,7 +644,6 @@ HROW
hsb
HSCROLL
hsi
-HSSH
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -691,12 +681,12 @@ iextn
IFACEMETHOD
IFACEMETHODIMP
IFile
-IGNOREBASECLASS
IGNOREUNKNOWN
IGo
iid
Iindex
Ijwhost
+ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
@@ -727,6 +717,7 @@ INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
+installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
@@ -740,8 +731,6 @@ Inste
Interlop
INTRESOURCE
INVALIDARG
-INVALIDCALL
-INVALIDINDEX
invalidoperatioexception
ipcmanager
IPREVIEW
@@ -803,7 +792,6 @@ LEVELID
LExit
lhwnd
LIBID
-libraryincludes
LIMITSIZE
LIMITTEXT
lindex
@@ -905,7 +893,6 @@ MBM
MBR
MDICHILD
MDL
-mdpvm
mdtext
mdtxt
mdwn
@@ -961,6 +948,7 @@ mpmc
MRM
MRT
mru
+MSAL
msc
mscorlib
msctls
@@ -1024,6 +1012,7 @@ netsh
newcolor
NEWDIALOGSTYLE
NEWFILE
+NEWFILEHEADER
newitem
newpath
newplus
@@ -1053,11 +1042,11 @@ NOCRLF
nodeca
NODRAWCAPTION
NODRAWICON
-NOFIXUPS
NOINHERITLAYOUT
NOINTERFACE
NOINVERT
NOLINKINFO
+nologo
NOMCX
NOMINMAX
NOMIRRORBITMAP
@@ -1069,13 +1058,11 @@ NONELEVATED
NONINFRINGEMENT
nonspace
nonstd
-NOOPEN
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
NOREDIRECTIONBITMAP
NOREDRAW
-NOREMAPCLSID
NOREMOVE
norename
NOREPEAT
@@ -1094,11 +1081,10 @@ NOTIFYICONDATAW
NOTIMPL
NOTOPMOST
NOTRACK
-NOTRUNCATE
NOTSRCCOPY
NOTSRCERASE
NOTXORPEN
-NOUSERSETTINGS
+notwindows
NOZORDER
NPH
npmjs
@@ -1224,6 +1210,7 @@ Podcasts
POINTERID
POINTERUPDATE
Pokedex
+Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
@@ -1292,6 +1279,7 @@ pstm
PStr
pstream
pstrm
+pswd
PSYSTEM
psz
ptb
@@ -1318,11 +1306,11 @@ Quarternary
QUERYENDSESSION
QUERYOPEN
QUEUESYNC
-QUICKTIP
QUNS
QXZ
RAII
RAlt
+Rappl
randi
Rasterization
Rasterize
@@ -1347,7 +1335,6 @@ REFCLSID
REFIID
REGCLS
regfile
-REGISTERCLASSEX
REGISTERCLASSFAILED
REGISTRYHEADER
registrypath
@@ -1359,7 +1346,6 @@ REINSTALLMODE
reloadable
Relogger
remappings
-REMAPRUNDLL
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
@@ -1421,6 +1407,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
+sancov
SAVEFAILED
scanled
schedtasks
@@ -1436,10 +1423,10 @@ SDDL
SDKDDK
sdns
searchterm
-searchtext
SEARCHUI
SECONDARYDISPLAY
secpol
+securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1485,6 +1472,7 @@ shellex
SHELLEXECUTEINFO
SHELLEXECUTEINFOW
SHELLEXTENSION
+SHELLICONSIZE
SHELLNEWVALUE
SHFILEINFO
SHFILEOPSTRUCT
@@ -1493,9 +1481,9 @@ SHGDNF
SHGFI
SHGFIICON
SHGFILARGEICON
+SHIL
shinfo
shlwapi
-SHNAMEMAPPING
shobjidl
SHORTCUTATLEAST
SHORTCUTMAXONEACTIONKEY
@@ -1525,6 +1513,7 @@ SIDs
siex
sigdn
SIGNINGSCENARIO
+signtool
Signtool
SINGLEKEY
sipolicy
@@ -1546,7 +1535,6 @@ SNAPPROCESS
snwprintf
softline
SOURCECLIENTAREAONLY
-sourced
sourcedoc
SOURCEHEADER
sourcesdirectory
@@ -1592,6 +1580,7 @@ stdcpp
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
+steamapps
STGC
STGM
STGMEDIUM
@@ -1673,6 +1662,7 @@ telephon
templatenamespace
testprocess
TEXCOORD
+TEXTBOXNEWLINE
TEXTEXTRACTOR
TEXTINCLUDE
tfopen
@@ -1685,7 +1675,6 @@ THotkey
throughs
TIcon
TILEDWINDOW
-TILEINFO
TILLSON
timedate
timediff
@@ -1699,9 +1688,6 @@ TLayout
tlb
tlbimp
tlc
-TPMLEFTALIGN
-TPMRETURNCMD
-TMPVAR
TNP
Toolhelp
toolkitconverters
@@ -1709,6 +1695,8 @@ toolwindow
TOPDOWNDIB
TOUCHEVENTF
TOUCHINPUT
+TPMLEFTALIGN
+TPMRETURNCMD
TRACEHANDLE
tracelogging
tracerpt
@@ -1782,7 +1770,6 @@ USRDLL
UType
uuidv
uwp
-ums
uxt
uxtheme
vabdq
@@ -1809,7 +1796,6 @@ VERTSIZE
VFT
vget
vgetq
-videourl
viewmodel
VIRTKEY
VIRTUALDESK
@@ -1843,7 +1829,7 @@ vstprintf
VSTT
vswhere
Vtbl
-WANTMAPPINGHANDLE
+WANTNUKEWARNING
WANTPALM
wasdk
wbem
@@ -1958,7 +1944,7 @@ WUX
Wwanpp
XAxis
xclip
-xdoc
+xcopy
XDocument
XElement
xfd
@@ -1981,13 +1967,13 @@ Yeet
YIncrement
yinle
yinyue
-youtube
YPels
YResolution
YStr
YTM
YVIRTUALSCREEN
ZEROINIT
+Zhiwei
zonable
zoneset
Zoneszonabletester
@@ -1997,3 +1983,11 @@ ZOOMITX
ZXk
ZXNs
zzz
+ACIE
+AOklab
+BCIE
+BOklab
+culori
+Evercoder
+LCh
+CIELCh
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000000..0db3dc6595
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,26 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request,
+# surfacing known-vulnerable versions of the packages declared or updated in the PR.
+# Once installed, if the workflow run is marked as required,
+# PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# As recommended by Microsoft's security guidelines (https://docs.opensource.microsoft.com/security/tsg/actions/#requirements-for-security-hardening-your-own-github-actions),
+# 3rd-party actions should be pinned to a specific commit hash to prevent supply chain attacks.
+# This update aligns with best practices; 1st/2nd-party actions is not required hash pinning.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 'Checkout Repository'
+ uses: actions/checkout@v4
+ - name: 'Dependency Review'
+ uses: actions/dependency-review-action@v4
\ No newline at end of file
diff --git a/.github/workflows/spelling2.yml b/.github/workflows/spelling2.yml
index e79708399f..03bb66fff6 100644
--- a/.github/workflows/spelling2.yml
+++ b/.github/workflows/spelling2.yml
@@ -93,7 +93,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
- uses: check-spelling/check-spelling@v0.0.24
+ uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
@@ -156,7 +156,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
steps:
- name: comment
- uses: check-spelling/check-spelling@v0.0.24
+ uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
checkout: true
@@ -175,7 +175,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
- uses: check-spelling/check-spelling@v0.0.24
+ uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
config: .github/actions/spell-check
checkout: true
@@ -202,7 +202,7 @@ jobs:
cancel-in-progress: false
steps:
- name: apply spelling updates
- uses: check-spelling/check-spelling@v0.0.24
+ uses: check-spelling/check-spelling@67debf50669c7fc76fc8f5d7f996384535a72b77 # v0.0.24
with:
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
checkout: true
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index cffdb0144a..15542cd500 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -221,6 +221,7 @@
"WinUI3Apps\\PowerToys.Settings.exe",
"PowerToys.CmdPalModuleInterface.dll",
+ "CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix"
],
"SigningInfo": {
@@ -331,6 +332,8 @@
"TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\OpenAI.dll",
+ "Testably.Abstractions.FileSystem.Interface.dll",
+ "WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
"ColorCode.Core.dll",
"ColorCode.UWP.dll",
"UnitsNet.dll",
diff --git a/.pipelines/UpdateVersions.ps1 b/.pipelines/UpdateVersions.ps1
index c19dfb5dec..a1bc5bef9a 100644
--- a/.pipelines/UpdateVersions.ps1
+++ b/.pipelines/UpdateVersions.ps1
@@ -1,16 +1,24 @@
Param(
- # Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
+ # Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
- [string]$winAppSdkVersionNumber = "1.6",
+ [string]$winAppSdkVersionNumber = "1.7",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
- [boolean]$useExperimentalVersion = $False
+ [boolean]$useExperimentalVersion = $False,
+
+ # Root folder Path for processing
+ [Parameter(Mandatory=$False,Position=3)]
+ [string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
+
+ # Root folder Path for processing
+ [Parameter(Mandatory=$False,Position=4)]
+ [string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
)
function Update-NugetConfig {
param (
- [string]$filePath = "nuget.config"
+ [string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
)
Write-Host "Updating nuget.config file"
@@ -35,7 +43,33 @@ function Update-NugetConfig {
$xml.Save($filePath)
}
-$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
+function Read-FileWithEncoding {
+ param (
+ [string]$Path
+ )
+
+ $reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
+ $content = $reader.ReadToEnd()
+ $encoding = $reader.CurrentEncoding
+ $reader.Close()
+
+ return [PSCustomObject]@{
+ Content = $content
+ Encoding = $encoding
+ }
+}
+
+function Write-FileWithEncoding {
+ param (
+ [string]$Path,
+ [string]$Content,
+ [System.Text.Encoding]$Encoding
+ )
+
+ $writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
+ $writer.Write($Content)
+ $writer.Close()
+}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
@@ -79,50 +113,54 @@ if ($latestVersion) {
}
# Update packages.config files
-Get-ChildItem -Recurse packages.config | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update Directory.Packages.props file
-$propsFile = "Directory.Packages.props"
+$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
if (Test-Path $propsFile) {
- $content = Get-Content $propsFile -Raw
+ $file = Read-FileWithEncoding -Path $propsFile
+ $content = $file.Content
if ($content -match ''
$oldVersionString = ''
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $propsFile -Value $content
+ Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
Write-Host "Modified " $propsFile
}
}
# Update .vcxproj files
-Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
-Get-ChildItem -Recurse *.csproj | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
diff --git a/.pipelines/tsa.json b/.pipelines/tsa.json
index 351545613f..2f1e84c7f1 100644
--- a/.pipelines/tsa.json
+++ b/.pipelines/tsa.json
@@ -3,5 +3,5 @@
"notificationAliases": ["powertoys@microsoft.com"],
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
- "areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
+ "areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
}
diff --git a/.pipelines/v2/ci-using-the-latest-winappsdk.yml b/.pipelines/v2/ci-using-the-latest-winappsdk.yml
index cc9f00f80d..3d628b13a9 100644
--- a/.pipelines/v2/ci-using-the-latest-winappsdk.yml
+++ b/.pipelines/v2/ci-using-the-latest-winappsdk.yml
@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
- default: 1.6
+ default: 1.7
- name: useExperimentalVersion
type: boolean
default: false
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index 26819c5d14..2a5be93aec 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -148,5 +148,7 @@ extends:
parameters:
versionNumber: ${{ parameters.versionNumber }}
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
+ ${{ if ne(parameters.publishSymbolsToPublic, true) }}:
+ symbolExpiryTime: 10 # For private builds, expire symbols within 10 days. The default is 100 years.
subscription: $(SymbolPublishingServiceConnection)
symbolProject: $(SymbolPublishingProject)
diff --git a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
index 1fccd6de74..9c59312844 100644
--- a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
+++ b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
@@ -17,6 +17,7 @@ steps:
arguments: >
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
+ -rootPath "$(build.sourcesdirectory)"
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'
diff --git a/COMMUNITY.md b/COMMUNITY.md
index 3e7cd6719e..ccfa61decf 100644
--- a/COMMUNITY.md
+++ b/COMMUNITY.md
@@ -188,6 +188,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
- [@nguyen-dows](https://github.com/nguyen-dows) - Christopher Nguyen - Product Manager
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
+- [@zhiwei-ms](https://github.com/zhiwei-ms) - Zhiwei Yu - Product Manager
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev lead
- [@yeelam-gordon](https://github.com/yeelam-gordon) - Gordon Lam - Dev lead
- [@jamrobot](https://github.com/jamrobot) - Jerry Xu - Dev lead
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f4cfe643e9..b3b404ea51 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,14 +10,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -29,7 +29,7 @@
-
+
@@ -45,7 +45,7 @@
-
+
@@ -55,13 +55,13 @@
-->
-
+
-
+
@@ -69,7 +69,7 @@
-
+
@@ -84,14 +84,17 @@
-
-
+
+
+
+
+
diff --git a/NOTICE.md b/NOTICE.md
index 15693c9590..d604be7d7f 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to
```
+## Utility: Command Palette Built-in Extensions
+
+### Calculator
+
+#### Mages
+
+We use the Mages NuGet package for calculating the result of expression.
+
+**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2016 - 2025 Florian Rappl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
## Utility: File Explorer Add-ins
### Monaco Editor
@@ -1393,7 +1427,6 @@ SOFTWARE.
## NuGet Packages used by PowerToys
-
- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta
- AdaptiveCards.Rendering.WinUI3 2.1.0-beta
- AdaptiveCards.Templating 2.0.2
@@ -1401,14 +1434,14 @@ SOFTWARE.
- Azure.AI.OpenAI 1.0.0-beta.17
- CommunityToolkit.Common 8.4.0
- CommunityToolkit.Mvvm 8.4.0
-- CommunityToolkit.WinUI.Animations 8.2.250129-preview2
-- CommunityToolkit.WinUI.Collections 8.2.250129-preview2
-- CommunityToolkit.WinUI.Controls.Primitives 8.2.250129-preview2
-- CommunityToolkit.WinUI.Controls.Segmented 8.2.250129-preview2
-- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250129-preview2
-- CommunityToolkit.WinUI.Controls.Sizers 8.2.250129-preview2
-- CommunityToolkit.WinUI.Converters 8.2.250129-preview2
-- CommunityToolkit.WinUI.Extensions 8.2.250129-preview2
+- CommunityToolkit.WinUI.Animations 8.2.250402
+- CommunityToolkit.WinUI.Collections 8.2.250402
+- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402
+- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402
+- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402
+- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402
+- CommunityToolkit.WinUI.Converters 8.2.250402
+- CommunityToolkit.WinUI.Extensions 8.2.250402
- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2
- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2
- ControlzEx 6.0.0
@@ -1419,7 +1452,7 @@ SOFTWARE.
- LazyCache 2.4.0
- Mages 3.0.0
- Markdig.Signed 0.34.0
-- MessagePack 2.5.187
+- MessagePack 3.1.3
- Microsoft.Bcl.AsyncInterfaces 9.0.4
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
- Microsoft.Data.Sqlite 9.0.4
@@ -1439,20 +1472,20 @@ SOFTWARE.
- Microsoft.Windows.CsWin32 0.2.46-beta
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
-- Microsoft.WindowsAppSDK 1.6.250205002
-- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
+- Microsoft.WindowsAppSDK 1.7.250401001
+- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4
- Moq 4.18.4
-- MSTest 3.6.3
+- MSTest 3.8.3
- NLog.Extensions.Logging 5.3.8
- NLog.Schema 5.2.8
- OpenAI 2.0.0
- ReverseMarkdown 4.1.0
- ScipBe.Common.Office.OneNote 3.0.1
- SharpCompress 0.37.2
-- StreamJsonRpc 2.19.27
+- StreamJsonRpc 2.21.69
- StyleCop.Analyzers 1.2.0-beta.556
- System.CodeDom 9.0.4
- System.CommandLine 2.0.0-beta4.22272.1
@@ -1463,17 +1496,21 @@ SOFTWARE.
- System.Diagnostics.EventLog 9.0.4
- System.Diagnostics.PerformanceCounter 9.0.4
- System.Drawing.Common 9.0.4
-- System.IO.Abstractions 21.0.29
-- System.IO.Abstractions.TestingHelpers 21.0.29
+- System.IO.Abstractions 22.0.13
+- System.IO.Abstractions.TestingHelpers 22.0.13
- System.Management 9.0.4
+- System.Net.Http 4.3.4
+- System.Private.Uri 4.3.2
- System.Reactive 6.0.1
- System.Runtime.Caching 9.0.4
- System.ServiceProcess.ServiceController 9.0.4
- System.Text.Encoding.CodePages 9.0.4
- System.Text.Json 9.0.4
+- System.Text.RegularExpressions 4.3.1
- UnicodeInformation 2.6.0
- UnitsNet 5.56.0
- UTF.Unknown 2.5.1
- WinUIEx 2.2.0
- WPF-UI 3.0.5
- WyHash 1.0.5
+
diff --git a/PowerToys.sln b/PowerToys.sln
index 9b911b388b..201c0fbefe 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -624,19 +624,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPalette", "CommandPa
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Built-in Extensions", "Built-in Extensions", "{ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extension SDK", "Extension SDK", "{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}"
EndProject
@@ -650,9 +650,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "sr
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample Extensions", "Sample Extensions", "{071E18A4-A530-46B8-AB7D-B862EE55E24E}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\Exts\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\ext\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\Exts\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\ext\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{7520A2FE-00A2-49B8-83ED-DB216E874C04}"
EndProject
@@ -660,7 +660,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "src\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{C66020D1-CB10-4CF7-8715-84C97FD5E5E2}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalModuleInterface", "src\modules\cmdpal\CmdPalModuleInterface\CmdPalModuleInterface.vcxproj", "{0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}"
EndProject
@@ -668,15 +668,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}"
EndProject
@@ -690,7 +690,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "sr
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}"
EndProject
@@ -704,7 +704,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\module
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests", "src\modules\registrypreview\RegistryPreview.FuzzTests\RegistryPreview.FuzzTests.csproj", "{5702B3CC-8575-48D5-83D8-15BB42269CD3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -2574,14 +2578,24 @@ Global
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.ActiveCfg = Debug|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.ActiveCfg = Release|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.Build.0 = Release|x64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.Build.0 = Debug|ARM64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.ActiveCfg = Debug|x64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.Build.0 = Debug|x64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.ActiveCfg = Release|ARM64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2852,6 +2866,8 @@ Global
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
+ {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/doc/Fuzzing/CppFuzzingGuide.md b/doc/Fuzzing/CppFuzzingGuide.md
new file mode 100644
index 0000000000..448d60a7d4
--- /dev/null
+++ b/doc/Fuzzing/CppFuzzingGuide.md
@@ -0,0 +1,117 @@
+# 🧪 C++ Project Fuzzing Test Guide
+
+This guide walks you through setting up a **fuzzing test** project for a C++ module using [libFuzzer](https://llvm.org/docs/LibFuzzer.html).
+.
+
+---
+
+## 🏗️ Step-by-Step Setup
+
+### 1. Create a New C++ Project
+
+- Use **Empty Project** template.
+- Name it `.FuzzingTest`.
+
+---
+
+### 2. Update Build Configuration
+
+- In **Configuration Manager**, Uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations.
+- Note: ARM64 is not supported in this case, so leave ARM64 configurations build disabled.
+---
+
+### 3. Enable ASan and libFuzzer in `.vcxproj`
+
+Edit the project file to enable fuzzing:
+
+```xml
+
+ true
+ true
+
+```
+
+---
+
+### 4. Add Fuzzing Compiler Flags
+
+Add this to `AdditionalOptions` under the `Fuzzing` configuration:
+
+```xml
+/fsanitize=address
+/fsanitize-coverage=inline-8bit-counters
+/fsanitize-coverage=edge
+/fsanitize-coverage=trace-cmp
+/fsanitize-coverage=trace-div
+%(AdditionalOptions)
+```
+
+---
+
+### 5. Link the Sanitizer Coverage Runtime
+
+In `Linker → Input → Additional Dependencies`, add:
+
+```text
+$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
+```
+
+---
+
+### 6. Copy Required Runtime DLL
+
+Add a `PostBuildEvent` to copy the ASAN DLL:
+
+```xml
+
+ xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
+
+```
+
+---
+
+### 7. Add Preprocessor Definitions
+
+To avoid annotation issues, add these to the `Preprocessor Definitions`:
+
+```text
+_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
+```
+
+---
+
+## 🧬 Required Code
+
+### `LLVMFuzzerTestOneInput` Entry Point
+
+Every fuzzing project must expose this function:
+
+```cpp
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ std::string input(reinterpret_cast(data), size);
+
+ try
+ {
+ // Call your module with the input here.
+ }
+ catch (...) {}
+
+ return 0;
+}
+```
+
+---
+
+## ⚙️ [Test run in the cloud](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/faq/notwindows/walkthrough)
+
+To submit a job to the cloud you can run with this command:
+
+```
+oip submit --config .\OneFuzzConfig.json --drop-path --platform windows --do-not-file-bugs --duration 1
+```
+You want to run with --do-not-file-bugs because if there is an issue with running the parser in the cloud (which is very possible), you don't want bugs to be created if there is an issue. The --duration task is the number of hours you want the task to run. I recommend just running for 1 hour to make sure things work initially. If you don't specify this parameter, it will default to 48 hours. You can find more about submitting a test job here.
+
+OneFuzz will send you an email when the job has started.
+
+---
diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md
index 90966ead20..ec8ffc69a4 100644
--- a/doc/thirdPartyRunPlugins.md
+++ b/doc/thirdPartyRunPlugins.md
@@ -43,6 +43,9 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [TailwindCSS](https://github.com/skttl/ptrun-tailwindcss) | [skttl](https://github.com/skttl) | Search the documentation of TailwindCSS |
| [HttpStatusCodes](https://github.com/grzhan/HttpStatusCodePowerToys) | [grzhan](https://github.com/grzhan) | Search for http status codes |
| [SVGL](https://github.com/Sameerjs6/powertoys-svgl) | [SameerJS6](https://github.com/SameerJS6) | Search, Browse and copy SVG logos from SVGL. |
+| [QuickNotes](https://github.com/ruslanlap/CommunityPowerToysRunPlugin-QuickNotes) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and search notes directly from PowerToys Run. |
+| [Weather](https://github.com/ruslanlap/PowerToysRun-Weather) | [ruslanlap](https://github.com/ruslanlap) | Get real-time weather information directly from PowerToys Run. |
+| [Pomodoro](https://github.com/ruslanlap/PowerToysRun-Pomodoro) | [ruslanlap](https://github.com/ruslanlap) | Manage Pomodoro productivity sessions directly from PowerToys Run. |
## Extending software plugins
@@ -65,3 +68,4 @@ Below are community created plugins that target a website or software. They are
| [Bilibili](https://github.com/Whuihuan/PowerToysRun-Bilibili) | [Whuihuan](https://github.com/Whuihuan) | Use AVID or BVID to parse and jump to Bilibili |
| [YubicoOauthOTP](https://github.com/dlnilsson/Community.PowerToys.Run.Plugin.YubicoOauthOTP) | [dlnilsson](https://github.com/dlnilsson) | Display generated codes from OATH accounts stored on the YubiKey in powerToys Run |
| [Firefox Bookmark](https://github.com/8LWXpg/PowerToysRun-FirefoxBookmark) | [8LWXpg](https://github.com/8LWXpg) | Open bookmarks in Firefox based browser |
+[Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 37256bdd68..f15b8a4714 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -79,10 +79,7 @@
-
-
-
-
+
diff --git a/src/Common.Dotnet.CsWinRT.props b/src/Common.Dotnet.CsWinRT.props
index e4731ce2fd..cde2ce69ce 100644
--- a/src/Common.Dotnet.CsWinRT.props
+++ b/src/Common.Dotnet.CsWinRT.props
@@ -1,6 +1,8 @@
+
+
10.0.22621.57
net9.0-windows10.0.22621.0
diff --git a/src/Common.Dotnet.PrepareGeneratedFolder.targets b/src/Common.Dotnet.PrepareGeneratedFolder.targets
new file mode 100644
index 0000000000..d017590064
--- /dev/null
+++ b/src/Common.Dotnet.PrepareGeneratedFolder.targets
@@ -0,0 +1,16 @@
+
+
+
+
+
+ $(ProjectDir)obj\g
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/codeAnalysis/GlobalSuppressions.cs b/src/codeAnalysis/GlobalSuppressions.cs
index c05e5f8820..d5ff98e548 100644
--- a/src/codeAnalysis/GlobalSuppressions.cs
+++ b/src/codeAnalysis/GlobalSuppressions.cs
@@ -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.
@@ -63,8 +63,16 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")]
-// AOT
+// AOT MVVMTK0045
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "AdvancedPaste.ViewModels")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "HostsUILib")]
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "EnvironmentVariablesUILib")]
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.FilePreviewer")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.UI.Views")]
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0045:Using [ObservableProperty] on fields is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib")]
+
+// AOT MVVMTK0049
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "Peek.FilePreviewer")]
[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "type", Target = "~T:Peek.UI.Views.TitleBar")]
+[assembly: SuppressMessage("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "MVVMTK0049:Using [INotifyPropertyChanged] is not AOT compatible for WinRT", Justification = "Updated MVVM toolkit package introduced this.", Scope = "namespaceanddescendants", Target = "RegistryPreviewUILib")]
diff --git a/src/common/AllExperiments/Logger.cs b/src/common/AllExperiments/Logger.cs
index 7604618bdf..b9e1f20969 100644
--- a/src/common/AllExperiments/Logger.cs
+++ b/src/common/AllExperiments/Logger.cs
@@ -25,7 +25,7 @@ namespace AllExperiments
}
// Using InvariantCulture since this is used for a log file name
- var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
+ var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
diff --git a/src/common/ManagedCommon/ColorFormatHelper.cs b/src/common/ManagedCommon/ColorFormatHelper.cs
index 08e62b921d..471104f215 100644
--- a/src/common/ManagedCommon/ColorFormatHelper.cs
+++ b/src/common/ManagedCommon/ColorFormatHelper.cs
@@ -141,6 +141,40 @@ namespace ManagedCommon
return lab;
}
+ ///
+ /// Convert a given to a Oklab color
+ ///
+ /// The to convert
+ /// The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]
+ public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
+ {
+ var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
+ var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
+ return oklab;
+ }
+
+ ///
+ /// Convert a given to a Oklch color
+ ///
+ /// The to convert
+ /// The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]
+ public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
+ {
+ var oklab = ConvertToOklabColor(color);
+ var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
+
+ return oklch;
+ }
+
+ public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
+ {
+ // inverse companding, gamma correction must be undone
+ double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
+ double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
+ double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
+ return (rLinear, gLinear, bLinear);
+ }
+
///
/// Convert a given to a CIE XYZ color (XYZ)
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
@@ -156,10 +190,7 @@ namespace ManagedCommon
double g = color.G / 255d;
double b = color.B / 255d;
- // inverse companding, gamma correction must be undone
- double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
- double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
- double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
+ (double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
return (
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
@@ -210,6 +241,63 @@ namespace ManagedCommon
return (l, a, b);
}
+ ///
+ /// Convert a linear RGB color to an Oklab color.
+ /// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
+ /// and the implementation is based on https://bottosson.github.io/posts/oklab/
+ ///
+ /// Linear R value
+ /// Linear G value
+ /// Linear B value
+ /// The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]
+ private static (double Lightness, double ChromaticityA, double ChromaticityB)
+ GetOklabColorFromLinearRGB(double r, double g, double b)
+ {
+ double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
+ double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
+ double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
+
+ double l_ = Math.Cbrt(l);
+ double m_ = Math.Cbrt(m);
+ double s_ = Math.Cbrt(s);
+
+ return (
+ (0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
+ (1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
+ (0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
+ );
+ }
+
+ ///
+ /// Convert an Oklab color from Cartesian form to its polar form Oklch
+ /// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
+ ///
+ /// The
+ /// The
+ /// The
+ /// The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]
+ private static (double Lightness, double Chroma, double Hue)
+ GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
+ {
+ return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
+ }
+
+ ///
+ /// Convert a color in Cartesian form (Lab) to its polar form (LCh)
+ ///
+ /// The
+ /// The
+ /// The
+ /// The lightness, chroma, and hue angle
+ private static (double Lightness, double Chroma, double Hue)
+ GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
+ {
+ // Lab to LCh transformation
+ double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
+ double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
+ return (lightness, chroma, hue);
+ }
+
///
/// Convert a given to a natural color (hue, whiteness, blackness)
///
@@ -276,12 +364,17 @@ namespace ManagedCommon
{ "Br", 'p' }, // brightness percent
{ "In", 'p' }, // intensity percent
{ "Ll", 'p' }, // lightness (HSL) percent
- { "Lc", 'p' }, // lightness(CIELAB)percent
{ "Va", 'p' }, // value percent
{ "Wh", 'p' }, // whiteness percent
{ "Bn", 'p' }, // blackness percent
- { "Ca", 'p' }, // chromaticityA percent
- { "Cb", 'p' }, // chromaticityB percent
+ { "Lc", 'p' }, // lightness (CIE) percent
+ { "Ca", 'p' }, // chromaticityA (CIELAB) percent
+ { "Cb", 'p' }, // chromaticityB (CIELAB) percent
+ { "Lo", 'p' }, // lightness (Oklab/Oklch) percent
+ { "Oa", 'p' }, // chromaticityA (Oklab) percent
+ { "Ob", 'p' }, // chromaticityB (Oklab) percent
+ { "Oc", 'p' }, // chroma (Oklch) percent
+ { "Oh", 'p' }, // hue angle (Oklch) percent
{ "Xv", 'i' }, // X value int
{ "Yv", 'i' }, // Y value int
{ "Zv", 'i' }, // Z value int
@@ -424,6 +517,10 @@ namespace ManagedCommon
var (lightnessC, _, _) = ConvertToCIELABColor(color);
lightnessC = Math.Round(lightnessC, 2);
return lightnessC.ToString(CultureInfo.InvariantCulture);
+ case "Lo":
+ var (lightnessO, _, _) = ConvertToOklabColor(color);
+ lightnessO = Math.Round(lightnessO, 2);
+ return lightnessO.ToString(CultureInfo.InvariantCulture);
case "Wh":
var (_, whiteness, _) = ConvertToHWBColor(color);
whiteness = Math.Round(whiteness * 100);
@@ -440,6 +537,22 @@ namespace ManagedCommon
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
chromaticityB = Math.Round(chromaticityB, 2);
return chromaticityB.ToString(CultureInfo.InvariantCulture);
+ case "Oa":
+ var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
+ chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
+ return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
+ case "Ob":
+ var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
+ chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
+ return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
+ case "Oc":
+ var (_, chromaOklch, _) = ConvertToOklchColor(color);
+ chromaOklch = Math.Round(chromaOklch, 2);
+ return chromaOklch.ToString(CultureInfo.InvariantCulture);
+ case "Oh":
+ var (_, _, hueOklch) = ConvertToOklchColor(color);
+ hueOklch = Math.Round(hueOklch, 2);
+ return hueOklch.ToString(CultureInfo.InvariantCulture);
case "Xv":
var (x, _, _) = ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
@@ -495,8 +608,10 @@ namespace ManagedCommon
case "HSI": return "hsi(%Hu, %Si%, %In%)";
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
case "NCol": return "%Hn, %Wh%, %Bn%";
- case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
+ case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
+ case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
+ case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
case "Decimal": return "%Dv";
case "HEX Int": return "0xFF%ReX%GrX%BlX";
diff --git a/src/common/ManagedCommon/IdRecoveryHelper.cs b/src/common/ManagedCommon/IdRecoveryHelper.cs
new file mode 100644
index 0000000000..cd0fcb57a5
--- /dev/null
+++ b/src/common/ManagedCommon/IdRecoveryHelper.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ManagedCommon
+{
+ public static class IdRecoveryHelper
+ {
+ ///
+ /// Fixes invalid IDs in the given list by assigning unique values.
+ /// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
+ ///
+ /// The list of items that may contain invalid IDs.
+ public static void RecoverInvalidIds(IEnumerable items)
+ where T : class, IHasId
+ {
+ var idSet = new HashSet();
+ int newId = 0;
+ var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
+
+ // Iterate through the list and fix invalid IDs
+ foreach (var item in sortedItems)
+ {
+ // If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
+ if (!idSet.Add(item.Id))
+ {
+ // Find the next available unique ID
+ while (idSet.Contains(newId))
+ {
+ newId++;
+ }
+
+ item.Id = newId;
+ idSet.Add(newId); // Add the newly assigned ID to the set
+ }
+ }
+ }
+ }
+
+ public interface IHasId
+ {
+ int Id { get; set; }
+ }
+}
diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs
index b67d63c3b5..cd85d500e1 100644
--- a/src/common/ManagedCommon/Logger.cs
+++ b/src/common/ManagedCommon/Logger.cs
@@ -54,7 +54,7 @@ namespace ManagedCommon
Directory.CreateDirectory(applicationLogPath);
}
- var logFilePath = Path.Combine(applicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt");
+ var logFilePath = Path.Combine(applicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj
index 0da17d85b8..bc7a4ada54 100644
--- a/src/common/UITestAutomation/UITestAutomation.csproj
+++ b/src/common/UITestAutomation/UITestAutomation.csproj
@@ -14,6 +14,9 @@
+
+
+
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index dab7ad64a2..c880614237 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -8,49 +8,49 @@ struct LogSettings
inline const static std::wstring logLevelOption = L"logLevel";
inline const static std::string runnerLoggerName = "runner";
inline const static std::wstring logPath = L"Logs\\";
- inline const static std::wstring runnerLogPath = L"RunnerLogs\\runner-log.txt";
+ inline const static std::wstring runnerLogPath = L"RunnerLogs\\runner-log.log";
inline const static std::string actionRunnerLoggerName = "action-runner";
- inline const static std::wstring actionRunnerLogPath = L"RunnerLogs\\action-runner-log.txt";
+ inline const static std::wstring actionRunnerLogPath = L"RunnerLogs\\action-runner-log.log";
inline const static std::string updateLoggerName = "update";
- inline const static std::wstring updateLogPath = L"UpdateLogs\\update-log.txt";
+ inline const static std::wstring updateLogPath = L"UpdateLogs\\update-log.log";
inline const static std::string fileExplorerLoggerName = "FileExplorer";
- inline const static std::wstring fileExplorerLogPath = L"Logs\\file-explorer-log.txt";
+ inline const static std::wstring fileExplorerLogPath = L"Logs\\file-explorer-log.log";
inline const static std::string gcodePrevLoggerName = "GcodePrevHandler";
- inline const static std::wstring gcodePrevLogPath = L"logs\\FileExplorer_localLow\\GcodePreviewHandler\\gcode-prev-handler-log.txt";
+ inline const static std::wstring gcodePrevLogPath = L"logs\\FileExplorer_localLow\\GcodePreviewHandler\\gcode-prev-handler-log.log";
inline const static std::string gcodeThumbLoggerName = "GcodeThumbnailProvider";
- inline const static std::wstring gcodeThumbLogPath = L"logs\\FileExplorer_localLow\\GcodeThumbnailProvider\\gcode-thumbnail-provider-log.txt";
+ inline const static std::wstring gcodeThumbLogPath = L"logs\\FileExplorer_localLow\\GcodeThumbnailProvider\\gcode-thumbnail-provider-log.log";
inline const static std::string mdPrevLoggerName = "MDPrevHandler";
- inline const static std::wstring mdPrevLogPath = L"logs\\FileExplorer_localLow\\MDPrevHandler\\md-prev-handler-log.txt";
+ inline const static std::wstring mdPrevLogPath = L"logs\\FileExplorer_localLow\\MDPrevHandler\\md-prev-handler-log.log";
inline const static std::string monacoPrevLoggerName = "MonacoPrevHandler";
- inline const static std::wstring monacoPrevLogPath = L"logs\\FileExplorer_localLow\\MonacoPrevHandler\\monaco-prev-handler-log.txt";
+ inline const static std::wstring monacoPrevLogPath = L"logs\\FileExplorer_localLow\\MonacoPrevHandler\\monaco-prev-handler-log.log";
inline const static std::string pdfPrevLoggerName = "PdfPrevHandler";
- inline const static std::wstring pdfPrevLogPath = L"logs\\FileExplorer_localLow\\PdfPrevHandler\\pdf-prev-handler-log.txt";
+ inline const static std::wstring pdfPrevLogPath = L"logs\\FileExplorer_localLow\\PdfPrevHandler\\pdf-prev-handler-log.log";
inline const static std::string pdfThumbLoggerName = "PdfThumbnailProvider";
- inline const static std::wstring pdfThumbLogPath = L"logs\\FileExplorer_localLow\\PdfThumbnailProvider\\pdf-thumbnail-provider-log.txt";
+ inline const static std::wstring pdfThumbLogPath = L"logs\\FileExplorer_localLow\\PdfThumbnailProvider\\pdf-thumbnail-provider-log.log";
inline const static std::string qoiPrevLoggerName = "QoiPrevHandler";
- inline const static std::wstring qoiPrevLogPath = L"logs\\FileExplorer_localLow\\QoiPreviewHandler\\qoi-prev-handler-log.txt";
+ inline const static std::wstring qoiPrevLogPath = L"logs\\FileExplorer_localLow\\QoiPreviewHandler\\qoi-prev-handler-log.log";
inline const static std::string qoiThumbLoggerName = "QoiThumbnailProvider";
- inline const static std::wstring qoiThumbLogPath = L"logs\\FileExplorer_localLow\\QoiThumbnailProvider\\qoi-thumbnail-provider-log.txt";
+ inline const static std::wstring qoiThumbLogPath = L"logs\\FileExplorer_localLow\\QoiThumbnailProvider\\qoi-thumbnail-provider-log.log";
inline const static std::string stlThumbLoggerName = "StlThumbnailProvider";
- inline const static std::wstring stlThumbLogPath = L"logs\\FileExplorer_localLow\\StlThumbnailProvider\\stl-thumbnail-provider-log.txt";
+ inline const static std::wstring stlThumbLogPath = L"logs\\FileExplorer_localLow\\StlThumbnailProvider\\stl-thumbnail-provider-log.log";
inline const static std::string svgPrevLoggerName = "SvgPrevHandler";
- inline const static std::wstring svgPrevLogPath = L"logs\\FileExplorer_localLow\\SvgPrevHandler\\svg-prev-handler-log.txt";
+ inline const static std::wstring svgPrevLogPath = L"logs\\FileExplorer_localLow\\SvgPrevHandler\\svg-prev-handler-log.log";
inline const static std::string svgThumbLoggerName = "SvgThumbnailProvider";
- inline const static std::wstring svgThumbLogPath = L"logs\\FileExplorer_localLow\\SvgThumbnailProvider\\svg-thumbnail-provider-log.txt";
+ inline const static std::wstring svgThumbLogPath = L"logs\\FileExplorer_localLow\\SvgThumbnailProvider\\svg-thumbnail-provider-log.log";
inline const static std::string launcherLoggerName = "launcher";
- inline const static std::wstring launcherLogPath = L"LogsModuleInterface\\launcher-log.txt";
+ inline const static std::wstring launcherLogPath = L"LogsModuleInterface\\launcher-log.log";
inline const static std::string mouseWithoutBordersLoggerName = "mouseWithoutBorders";
- inline const static std::wstring mouseWithoutBordersLogPath = L"LogsModuleInterface\\mouseWithoutBorders-log.txt";
- inline const static std::wstring awakeLogPath = L"Logs\\awake-log.txt";
- inline const static std::wstring powerAccentLogPath = L"quick-accent-log.txt";
+ inline const static std::wstring mouseWithoutBordersLogPath = L"LogsModuleInterface\\mouseWithoutBorders-log.log";
+ inline const static std::wstring awakeLogPath = L"Logs\\awake-log.log";
+ inline const static std::wstring powerAccentLogPath = L"quick-accent-log.log";
inline const static std::string fancyZonesLoggerName = "fancyzones";
- inline const static std::wstring fancyZonesLogPath = L"fancyzones-log.txt";
+ inline const static std::wstring fancyZonesLogPath = L"fancyzones-log.log";
inline const static std::wstring fancyZonesOldLogPath = L"FancyZonesLogs\\"; // needed to clean up old logs
inline const static std::string shortcutGuideLoggerName = "shortcut-guide";
- inline const static std::wstring shortcutGuideLogPath = L"ShortcutGuideLogs\\shortcut-guide-log.txt";
- inline const static std::wstring powerOcrLogPath = L"Logs\\text-extractor-log.txt";
+ inline const static std::wstring shortcutGuideLogPath = L"ShortcutGuideLogs\\shortcut-guide-log.log";
+ inline const static std::wstring powerOcrLogPath = L"Logs\\text-extractor-log.log";
inline const static std::string keyboardManagerLoggerName = "keyboard-manager";
- inline const static std::wstring keyboardManagerLogPath = L"Logs\\keyboard-manager-log.txt";
+ inline const static std::wstring keyboardManagerLogPath = L"Logs\\keyboard-manager-log.log";
inline const static std::string findMyMouseLoggerName = "find-my-mouse";
inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter";
inline const static std::string mouseJumpLoggerName = "mouse-jump";
@@ -60,22 +60,22 @@ struct LogSettings
inline const static std::string alwaysOnTopLoggerName = "always-on-top";
inline const static std::string powerOcrLoggerName = "TextExtractor";
inline const static std::string fileLocksmithLoggerName = "FileLocksmith";
- inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt";
+ inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.log";
inline const static std::string hostsLoggerName = "hosts";
- inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt";
+ inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.log";
inline const static std::string registryPreviewLoggerName = "registrypreview";
inline const static std::string cropAndLockLoggerName = "crop-and-lock";
- inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt";
+ inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.log";
inline const static std::string environmentVariablesLoggerName = "environment-variables";
- inline const static std::wstring cmdNotFoundLogPath = L"Logs\\cmd-not-found-log.txt";
+ inline const static std::wstring cmdNotFoundLogPath = L"Logs\\cmd-not-found-log.log";
inline const static std::string cmdNotFoundLoggerName = "cmd-not-found";
inline const static std::string newLoggerName = "NewPlus";
inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher";
- inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt";
+ inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.log";
inline const static std::string workspacesWindowArrangerLoggerName = "workspaces-window-arranger";
- inline const static std::wstring workspacesWindowArrangerLogPath = L"workspaces-window-arranger-log.txt";
+ inline const static std::wstring workspacesWindowArrangerLogPath = L"workspaces-window-arranger-log.log";
inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
- inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt";
+ inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
inline const static std::string zoomItLoggerName = "zoom-it";
inline const static int retention = 30;
std::wstring logLevel;
diff --git a/src/common/utils/logger_helper.h b/src/common/utils/logger_helper.h
index c2207525e3..2deec22155 100644
--- a/src/common/utils/logger_helper.h
+++ b/src/common/utils/logger_helper.h
@@ -91,7 +91,7 @@ namespace LoggerHelpers
currentFolder.append(get_product_version());
auto logsPath = currentFolder;
- logsPath.append(L"log.txt");
+ logsPath.append(L"log.log");
Logger::init(loggerName, logsPath.wstring(), PTSettingsHelper::get_log_settings_file_location());
delete_other_versions_log_folders(rootFolder.wstring(), currentFolder);
diff --git a/src/common/utils/package.h b/src/common/utils/package.h
index 60bde7ea53..138f3b8e5b 100644
--- a/src/common/utils/package.h
+++ b/src/common/utils/package.h
@@ -301,6 +301,7 @@ namespace package
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
+ return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
index b56868ece8..218349b32b 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
@@ -18,10 +18,19 @@ public static class OcrHelpers
{
public static async Task ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
- var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
+ var ocrLanguage = GetOCRLanguage();
cancellationToken.ThrowIfCancellationRequested();
- var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
+ OcrEngine ocrEngine;
+ if (ocrLanguage is not null)
+ {
+ ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
+ }
+ else
+ {
+ ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
+ }
+
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
index 24820b7d83..3973f8d2eb 100644
--- a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
@@ -15,8 +15,6 @@
PowerToys.EnvironmentVariablesUILib.pri
true
true
-
- $(ProjectDir)obj\g
@@ -56,7 +54,4 @@
-
-
-
diff --git a/src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj b/src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj
index f9a7f4247b..e94a0d9b4b 100644
--- a/src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj
+++ b/src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj
@@ -20,6 +20,9 @@
+
+
+
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
index 86d258854f..f0b12187ad 100644
--- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
@@ -1,6 +1,6 @@
-
+
@@ -141,7 +141,7 @@
-
+
@@ -153,7 +153,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/packages.config b/src/modules/MeasureTool/MeasureToolCore/packages.config
index 61ff4b9f07..996736442d 100644
--- a/src/modules/MeasureTool/MeasureToolCore/packages.config
+++ b/src/modules/MeasureTool/MeasureToolCore/packages.config
@@ -4,5 +4,5 @@
-
+
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
index 4d4b47ea12..7075da2ea7 100644
--- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
+++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
@@ -121,6 +121,22 @@ namespace AppLauncher
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
+ {
+ Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
+ auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
+ if (res.isOk())
+ {
+ launched = true;
+ }
+ else
+ {
+ launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
+ }
+ }
+
+ // win32 app with appUserModelId:
+ // usage example: steam games
+ if (!launched && !app.appUserModelId.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
index 19b33214b7..a37d82f8ca 100644
--- a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
+++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
@@ -1,5 +1,6 @@
#include "pch.h"
#include "AppUtils.h"
+#include "SteamHelper.h"
#include
#include
@@ -34,6 +35,8 @@ namespace Utils
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
+
+ constexpr const wchar_t* SteamUrlProtocol = L"steam:";
}
AppList IterateAppsFolder()
@@ -138,6 +141,34 @@ namespace Utils
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
{
data.installPath = propVariantString.m_pData;
+
+ if (!data.installPath.empty())
+ {
+ const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
+
+ if (isSteamProtocol)
+ {
+ Logger::info(L"Found steam game: protocol path: {}", data.installPath);
+ data.protocolPath = data.installPath;
+
+ try
+ {
+ auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
+ auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
+
+ if (gameFolder)
+ {
+ data.installPath = gameFolder->gameInstallationPath;
+ Logger::info(L"Found steam game: physical path: {}", data.installPath);
+ }
+ }
+ catch (std::exception ex)
+ {
+ Logger::error(L"Failed to get installPath for game {}", data.installPath);
+ Logger::error("Error: {}", ex.what());
+ }
+ }
+ }
}
}
@@ -397,5 +428,10 @@ namespace Utils
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}
+
+ bool AppData::IsSteamGame() const
+ {
+ return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
+ }
}
}
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.h b/src/modules/Workspaces/WorkspacesLib/AppUtils.h
index 3c81049f83..80b5e2fd49 100644
--- a/src/modules/Workspaces/WorkspacesLib/AppUtils.h
+++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.h
@@ -13,10 +13,12 @@ namespace Utils
std::wstring packageFullName;
std::wstring appUserModelId;
std::wstring pwaAppId;
+ std::wstring protocolPath;
bool canLaunchElevated = false;
bool IsEdge() const;
bool IsChrome() const;
+ bool IsSteamGame() const;
};
using AppList = std::vector;
diff --git a/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
new file mode 100644
index 0000000000..404002e284
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
@@ -0,0 +1,171 @@
+#include "pch.h"
+#include "SteamHelper.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Utils
+{
+
+ static std::wstring Utf8ToWide(const std::string& utf8)
+ {
+ if (utf8.empty())
+ return L"";
+
+ int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0);
+ if (size <= 0)
+ return L"";
+
+ std::wstring wide(size, L'\0');
+ MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), wide.data(), size);
+ return wide;
+ }
+
+ namespace Steam
+ {
+ using namespace std;
+ namespace fs = std::filesystem;
+
+ static std::optional GetSteamExePathFromRegistry()
+ {
+ static std::optional cachedPath;
+ if (cachedPath.has_value())
+ {
+ return cachedPath;
+ }
+
+ const std::vector roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
+ const std::vector subKeys = {
+ L"steam\\shell\\open\\command",
+ L"Software\\Classes\\steam\\shell\\open\\command",
+ };
+
+ for (HKEY root : roots)
+ {
+ for (const auto& subKey : subKeys)
+ {
+ HKEY hKey;
+ if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
+ {
+ wchar_t value[512];
+ DWORD size = sizeof(value);
+ DWORD type = 0;
+
+ if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast(value), &size) == ERROR_SUCCESS &&
+ (type == REG_SZ || type == REG_EXPAND_SZ))
+ {
+ std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
+ std::wcmatch match;
+ if (std::regex_search(value, match, exeRegex) && match.size() > 1)
+ {
+ RegCloseKey(hKey);
+ cachedPath = match[1].str();
+ return cachedPath;
+ }
+ }
+
+ RegCloseKey(hKey);
+ }
+ }
+ }
+
+ cachedPath = std::nullopt;
+ return std::nullopt;
+ }
+
+ static fs::path GetSteamBasePath()
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
+ }
+
+ static fs::path GetAcfFilePath(const std::wstring& gameId)
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
+ }
+
+ static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return GetSteamBasePath() / L"common" / gameFolderName;
+ }
+
+ static unordered_map ParseAcfFile(const fs::path& acfPath)
+ {
+ unordered_map result;
+
+ ifstream file(acfPath);
+ if (!file.is_open())
+ return result;
+
+ string line;
+ while (getline(file, line))
+ {
+ smatch matches;
+ static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
+
+ if (regex_search(line, matches, pattern) && matches.size() == 3)
+ {
+ wstring key = Utf8ToWide(matches[1].str());
+ wstring value = Utf8ToWide(matches[2].str());
+ result[key] = value;
+ }
+ }
+
+ return result;
+ }
+
+ std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
+ {
+ fs::path acfPath = Steam::GetAcfFilePath(gameId);
+
+ if (!fs::exists(acfPath))
+ return nullptr;
+
+ auto kv = ParseAcfFile(acfPath);
+ if (kv.empty() || kv.find(L"installdir") == kv.end())
+ return nullptr;
+
+ fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
+ if (!fs::exists(gamePath))
+ return nullptr;
+
+ auto game = std::make_unique();
+ game->gameId = gameId;
+ game->gameInstallationPath = gamePath.wstring();
+ return game;
+ }
+
+ std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
+ {
+ const std::wstring steamGamePrefix = L"steam://rungameid/";
+
+ if (urlPath.rfind(steamGamePrefix, 0) == 0)
+ {
+ return urlPath.substr(steamGamePrefix.length());
+ }
+
+ return L"";
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLib/SteamHelper.h b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h
new file mode 100644
index 0000000000..a80a942f4a
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "pch.h"
+
+namespace Utils
+{
+ namespace NonLocalizable
+ {
+ const std::wstring AcfFileNameTemplate = L"appmanifest_.acfs";
+ }
+
+ namespace Steam
+ {
+ struct SteamGame
+ {
+ std::wstring gameId;
+ std::wstring gameInstallationPath;
+ };
+
+ std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
+
+ std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
index 27394e29ee..7d29741a0d 100644
--- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
+++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
@@ -41,6 +41,7 @@
+
@@ -57,6 +58,7 @@
Create
+
diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
index b066c16a57..f4f17c55ee 100644
--- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
+++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
@@ -53,6 +53,9 @@
Header Files
+
+ Header Files
+
@@ -88,6 +91,9 @@
Source Files
+
+ Source Files
+
diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
index 1d5bc8a179..a8b7c13108 100644
--- a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
+++ b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
@@ -71,6 +71,8 @@ namespace SnapshotUtils
continue;
}
+ Logger::info("Try to get window app:{}", reinterpret_cast(window));
+
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
@@ -118,10 +120,19 @@ namespace SnapshotUtils
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
if (!data.has_value() || data->name.empty())
{
- Logger::info(L"Installed app not found: {}", processPath);
+ Logger::info(L"Installed app not found:{},{}", reinterpret_cast(window), processPath);
continue;
}
+ if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
+ {
+ // Only care about steam games if it has no thick frame to remain consistent with
+ // the behavior as before.
+ continue;
+ }
+
+ Logger::info(L"Found app for window:{},{}", reinterpret_cast(window), processPath);
+
auto appData = data.value();
bool isEdge = appData.IsEdge();
diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
index 7b04135d1a..538579979f 100644
--- a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
+++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
@@ -200,6 +200,14 @@ std::optional WindowArranger::GetNearestWindow(const Workspa
}
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
+
+ if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
+ {
+ // Only care about steam games if it has no thick frame to remain consistent with
+ // the behavior as before.
+ continue;
+ }
+
if (!data.has_value())
{
continue;
diff --git a/src/modules/Workspaces/workspaces-common/WindowFilter.h b/src/modules/Workspaces/workspaces-common/WindowFilter.h
index c76ad81237..8ae1a5411b 100644
--- a/src/modules/Workspaces/workspaces-common/WindowFilter.h
+++ b/src/modules/Workspaces/workspaces-common/WindowFilter.h
@@ -9,10 +9,12 @@ namespace WindowFilter
{
auto style = GetWindowLong(window, GWL_STYLE);
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
- bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
- if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
+
+ Logger::info("Style for window: {}, {:#x}", reinterpret_cast(window), style);
+
+ if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
{
// popup windows we want to snap: e.g. Calculator, Telegram
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
diff --git a/src/modules/Workspaces/workspaces-common/WindowUtils.h b/src/modules/Workspaces/workspaces-common/WindowUtils.h
index 8424591dfa..79051f4ea2 100644
--- a/src/modules/Workspaces/workspaces-common/WindowUtils.h
+++ b/src/modules/Workspaces/workspaces-common/WindowUtils.h
@@ -121,4 +121,11 @@ namespace WindowUtils
return std::wstring(title);
}
+
+
+ inline bool HasThickFrame(HWND window)
+ {
+ auto style = GetWindowLong(window, GWL_STYLE);
+ return WindowUtils::HasStyle(style, WS_THICKFRAME);
+ }
}
\ No newline at end of file
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.def b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.def
new file mode 100644
index 0000000000..24e7c1235c
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.def
@@ -0,0 +1,3 @@
+EXPORTS
+DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
+DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.rc b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.rc
new file mode 100644
index 0000000000..5fa3c8b90d
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.rc
@@ -0,0 +1,40 @@
+#include
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+#include "winres.h"
+#undef APSTUDIO_READONLY_SYMBOLS
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj
new file mode 100644
index 0000000000..e110d736ae
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj
@@ -0,0 +1,187 @@
+
+
+
+
+ true
+ true
+ {5f63c743-f6ce-4dba-a200-2b3f8a14e8c2}
+ CmdPalKeyboardService
+ CmdPalKeyboardService
+ false
+
+
+
+
+ true
+ false
+
+
+
+
+ true
+ true
+ en-US
+ 17.0
+ 10.0
+
+
+ true
+ true
+ true
+ Windows Store
+ false
+
+
+
+ <_VC_Target_Library_Platform>Desktop
+ <_NoWinAPIFamilyApp>true
+
+
+
+
+
+ DynamicLibrary
+ v143
+ Unicode
+ false
+
+
+ true
+ true
+
+
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CmdPalKeyboardService
+ ..\..\..\..\$(Platform)\$(Configuration)\
+
+
+
+ $(IntDir)pch.pch
+ Level4
+ %(AdditionalOptions) /bigobj
+ _WINRT_DLL;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)
+ ../../..;%(AdditionalIncludeDirectories)
+ $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)
+
+
+ Console
+ false
+ CmdPalKeyboardService.def
+ Shell32.lib;user32.lib;%(AdditionalDependencies)
+
+
+
+
+ _DEBUG;%(PreprocessorDefinitions)
+
+
+
+
+ NDEBUG;%(PreprocessorDefinitions)
+
+
+ true
+ true
+
+
+
+
+
+ KeyboardListener.idl
+
+
+
+
+
+ Create
+
+
+ KeyboardListener.idl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
+
+
+
+ MultiThreadedDebug
+
+
+
+ %(IgnoreSpecificDefaultLibraries);libucrtd.lib
+ %(AdditionalOptions) /defaultlib:ucrtd.lib
+
+
+
+
+
+ MultiThreaded
+
+
+
+ %(IgnoreSpecificDefaultLibraries);libucrt.lib
+ %(AdditionalOptions) /defaultlib:ucrt.lib
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj.filters b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj.filters
new file mode 100644
index 0000000000..301ac57f3a
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj.filters
@@ -0,0 +1,36 @@
+
+
+
+
+ accd3aa8-1ba0-4223-9bbe-0c431709210b
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {926ab91d-31b4-48c3-b9a4-e681349f27f0}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resources
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.cpp b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.cpp
new file mode 100644
index 0000000000..cf52fe1863
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.cpp
@@ -0,0 +1,165 @@
+#include "pch.h"
+#include "KeyboardListener.h"
+#include "KeyboardListener.g.cpp"
+
+// #include
+// #include
+#include
+
+namespace
+{
+}
+
+namespace winrt::CmdPalKeyboardService::implementation
+{
+ KeyboardListener::KeyboardListener()
+ {
+ s_instance = this;
+ }
+
+ void KeyboardListener::Start()
+ {
+#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED)
+ const bool hook_disabled = IsDebuggerPresent();
+#else
+ const bool hook_disabled = false;
+#endif
+ if (!hook_disabled)
+ {
+ if (!s_llKeyboardHook)
+ {
+ s_llKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, NULL);
+ if (!s_llKeyboardHook)
+ {
+ DWORD errorCode = GetLastError();
+ show_last_error_message(L"SetWindowsHookEx", errorCode, L"CmdPalKeyboardService");
+ }
+ }
+ }
+ }
+
+ void KeyboardListener::Stop()
+ {
+ if (s_llKeyboardHook && UnhookWindowsHookEx(s_llKeyboardHook))
+ {
+ s_llKeyboardHook = NULL;
+ }
+ }
+
+ void KeyboardListener::SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id)
+ {
+ Hotkey hotkey = { .win = win, .ctrl = ctrl, .shift = shift, .alt = alt, .key = key };
+ std::unique_lock lock{ mutex };
+
+ HotkeyDescriptor desc = { .hotkey = hotkey, .id = std::wstring(id) };
+ hotkeyDescriptors.insert(desc);
+ }
+
+ void KeyboardListener::ClearHotkey(hstring const& id)
+ {
+ {
+ std::unique_lock lock{ mutex };
+ auto it = hotkeyDescriptors.begin();
+ while (it != hotkeyDescriptors.end())
+ {
+ if (it->id == id)
+ {
+ it = hotkeyDescriptors.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ }
+
+ void KeyboardListener::ClearHotkeys()
+ {
+ {
+ std::unique_lock lock{ mutex };
+ auto it = hotkeyDescriptors.begin();
+ while (it != hotkeyDescriptors.end())
+ {
+ it = hotkeyDescriptors.erase(it);
+ }
+ }
+ }
+
+ void KeyboardListener::SetProcessCommand(ProcessCommand processCommand)
+ {
+ m_processCommandCb = [trigger = std::move(processCommand)](hstring const& id) {
+ trigger(id);
+ };
+ }
+
+ LRESULT KeyboardListener::DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
+ {
+ const auto& keyPressInfo = *reinterpret_cast(lParam);
+
+ if ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN))
+ {
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+ }
+
+ Hotkey hotkey{
+ .win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000),
+ .ctrl = static_cast(GetAsyncKeyState(VK_CONTROL) & 0x8000),
+ .shift = static_cast(GetAsyncKeyState(VK_SHIFT) & 0x8000),
+ .alt = static_cast(GetAsyncKeyState(VK_MENU) & 0x8000),
+ .key = static_cast(keyPressInfo.vkCode)
+ };
+
+ if (hotkey == Hotkey{})
+ {
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+ }
+
+ bool do_action = false;
+ std::wstring actionId{};
+
+ {
+ // Hold the lock for the shortest possible duration
+ std::unique_lock lock{ mutex };
+ HotkeyDescriptor dummy{ .hotkey = hotkey };
+ auto it = hotkeyDescriptors.find(dummy);
+ if (it != hotkeyDescriptors.end())
+ {
+ do_action = true;
+ actionId = it->id;
+ }
+ }
+
+ if (do_action)
+ {
+ m_processCommandCb(hstring{ actionId });
+
+ // After invoking the hotkey send a dummy key to prevent Start Menu from activating
+ INPUT dummyEvent[1] = {};
+ dummyEvent[0].type = INPUT_KEYBOARD;
+ dummyEvent[0].ki.wVk = 0xFF;
+ dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP;
+ SendInput(1, dummyEvent, sizeof(INPUT));
+
+ // Swallow the key press
+ return 1;
+ }
+
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+ }
+
+ LRESULT KeyboardListener::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
+ {
+ if (s_instance == nullptr)
+ {
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+ }
+
+ if (nCode < 0)
+ {
+ return CallNextHookEx(NULL, nCode, wParam, lParam);
+ }
+
+ return s_instance->DoLowLevelKeyboardProc(nCode, wParam, lParam);
+ }
+}
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.h b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.h
new file mode 100644
index 0000000000..7cd82ba77c
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "KeyboardListener.g.h"
+#include
+#include
+#include
+
+namespace winrt::CmdPalKeyboardService::implementation
+{
+ struct KeyboardListener : KeyboardListenerT
+ {
+ struct Hotkey
+ {
+ bool win = false;
+ bool ctrl = false;
+ bool shift = false;
+ bool alt = false;
+ unsigned char key = 0;
+
+ std::strong_ordering operator<=>(const Hotkey&) const = default;
+ };
+
+ struct HotkeyDescriptor
+ {
+ Hotkey hotkey;
+ std::wstring id;
+
+ bool operator<(const HotkeyDescriptor& other) const
+ {
+ return hotkey < other.hotkey;
+ };
+ };
+
+ KeyboardListener();
+
+ void Start();
+ void Stop();
+ void SetHotkeyAction(bool win, bool ctrl, bool shift, bool alt, uint8_t key, hstring const& id);
+ void ClearHotkey(hstring const& id);
+ void ClearHotkeys();
+ void SetProcessCommand(ProcessCommand processCommand);
+
+ static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
+
+ private:
+ LRESULT CALLBACK DoLowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
+
+ static inline KeyboardListener* s_instance;
+ HHOOK s_llKeyboardHook = nullptr;
+
+ // Max DWORD for key code to disable keys.
+ const DWORD VK_DISABLED = 0x100;
+ DWORD vkCodePressed = VK_DISABLED;
+
+ std::multiset hotkeyDescriptors;
+ std::mutex mutex;
+
+ std::function m_processCommandCb;
+ };
+}
+
+namespace winrt::CmdPalKeyboardService::factory_implementation
+{
+ struct KeyboardListener : KeyboardListenerT
+ {
+ };
+}
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.idl b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.idl
new file mode 100644
index 0000000000..8d808e386c
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/KeyboardListener.idl
@@ -0,0 +1,16 @@
+
+namespace CmdPalKeyboardService
+{
+ [version(1.0), uuid(78ab07cd-e128-4e73-86aa-e48e6b6d01ff)] delegate void ProcessCommand(String id);
+
+ [default_interface] runtimeclass KeyboardListener {
+ KeyboardListener();
+ void Start();
+ void Stop();
+
+ void SetHotkeyAction(Boolean win, Boolean ctrl, Boolean shift, Boolean alt, UInt8 key, String id);
+ void ClearHotkey(String id);
+ void ClearHotkeys();
+ void SetProcessCommand(ProcessCommand processCommand);
+ }
+}
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/PropertySheet.props b/src/modules/cmdpal/CmdPalKeyboardService/PropertySheet.props
new file mode 100644
index 0000000000..27ad40e537
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/PropertySheet.props
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/packages.config b/src/modules/cmdpal/CmdPalKeyboardService/packages.config
new file mode 100644
index 0000000000..09bfc449e2
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/pch.cpp b/src/modules/cmdpal/CmdPalKeyboardService/pch.cpp
new file mode 100644
index 0000000000..bcb5590be1
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/pch.h b/src/modules/cmdpal/CmdPalKeyboardService/pch.h
new file mode 100644
index 0000000000..b10d0155ca
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/pch.h
@@ -0,0 +1,4 @@
+#pragma once
+#include
+#include
+#include
diff --git a/src/modules/cmdpal/CmdPalKeyboardService/resource.h b/src/modules/cmdpal/CmdPalKeyboardService/resource.h
new file mode 100644
index 0000000000..f99c3adb95
--- /dev/null
+++ b/src/modules/cmdpal/CmdPalKeyboardService/resource.h
@@ -0,0 +1,13 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by PowerToys.MeasureToolCore.rc
+
+//////////////////////////////
+// Non-localizable
+
+#define FILE_DESCRIPTION "CmdPalKeyboardService"
+#define INTERNAL_NAME "CmdPalKeyboardService"
+#define ORIGINAL_FILENAME "CmdPalKeyboardService.dll"
+
+// Non-localizable
+//////////////////////////////
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
index 4395e340fa..2d70013009 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
+++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
@@ -2,6 +2,7 @@
+
17.0
Win32Proj
@@ -49,13 +50,20 @@
- EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+ EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
+ %(PreprocessorDefinitions);
+
+
+ IS_DEV_BRANDING;%(PreprocessorDefinitions)
+
..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
$(OutDir)$(TargetName)$(TargetExt)
+
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
index aceadabdd8..bff7279b68 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
+++ b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
@@ -3,6 +3,7 @@
#include
+#include
#include
#include
#include
@@ -10,10 +11,11 @@
#include
#include
#include
+#include
#include
#include
#include
-#include
+#include
HINSTANCE g_hInst_cmdPal = 0;
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
class CmdPal : public PowertoyModuleIface
{
private:
- bool m_enabled = false;
-
std::wstring app_name;
//contains the non localized key of the powertoy
@@ -46,7 +46,10 @@ private:
HANDLE m_hTerminateEvent;
- void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
+ // Track if this is the first call to enable
+ bool firstEnableCall = true;
+
+ static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -54,6 +57,10 @@ private:
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
+ if (silentFail)
+ {
+ sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
+ }
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
@@ -64,7 +71,11 @@ private:
{
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
+ return false;
}
+
+ m_launched.store(true);
+ return true;
}
std::vector GetProcessesIdByName(const std::wstring& processName)
@@ -122,6 +133,9 @@ private:
}
public:
+ static std::atomic m_enabled;
+ static std::atomic m_launched;
+
CmdPal()
{
app_name = L"CmdPal";
@@ -133,10 +147,7 @@ public:
~CmdPal()
{
- if (m_enabled)
- {
- }
- m_enabled = false;
+ CmdPal::m_enabled.store(false);
}
// Destroy the powertoy and free memory
@@ -203,16 +214,23 @@ public:
{
Logger::trace("CmdPal::enable()");
- m_enabled = true;
+ CmdPal::m_enabled.store(true);
- try
+ std::wstring packageName = L"Microsoft.CommandPalette";
+ std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
+#ifdef IS_DEV_BRANDING
+ packageName = L"Microsoft.CommandPalette.Dev";
+ launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
+#endif
+
+ if (!package::GetRegisteredPackage(packageName, false).has_value())
{
- if (!package::GetRegisteredPackage(L"Microsoft.CommandPalette", false).has_value())
+ try
{
Logger::info(L"CmdPal not installed. Installing...");
std::wstring installationFolder = get_module_folderpath();
-#if _DEBUG
+#ifdef _DEBUG
std::wstring archSubdir = L"x64";
#ifdef _M_ARM64
archSubdir = L"ARM64";
@@ -234,19 +252,34 @@ public:
}
}
}
- }
- catch (std::exception& e)
- {
- std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
- errorMessage += e.what();
- Logger::error(errorMessage);
+ catch (std::exception& e)
+ {
+ std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
+ errorMessage += e.what();
+ Logger::error(errorMessage);
+ }
}
-#if _DEBUG
- LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
-#else
- LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
-#endif
+ if (!package::GetRegisteredPackage(packageName, false).has_value())
+ {
+ Logger::error("Cmdpal is not registered, quit..");
+ return;
+ }
+
+ if (!firstEnableCall)
+ {
+ Logger::trace("Not first attempt, try to launch");
+ LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
+ }
+ else
+ {
+ // If first time enable, do retry launch.
+ Logger::trace("First attempt, try to launch");
+ std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
+ launchThread.detach();
+ }
+
+ firstEnableCall = false;
}
virtual void disable()
@@ -254,7 +287,44 @@ public:
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
- m_enabled = false;
+ CmdPal::m_enabled.store(false);
+ }
+
+ static void RetryLaunch(std::wstring path)
+ {
+ const int base_delay_milliseconds = 1000;
+ int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
+ int retry = 0;
+ do
+ {
+ auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
+ if (launch_result)
+ {
+ Logger::info(L"CmdPal launched successfully after {} retries.", retry);
+ return;
+ }
+ else
+ {
+ Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
+ }
+
+ // When we got max retry, we don't need to wait for the next retry.
+ if (retry < max_retry)
+ {
+ int delay = base_delay_milliseconds * (1 << (retry));
+ std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+ }
+ ++retry;
+ } while (retry <= max_retry && m_enabled.load() && !m_launched.load());
+
+ if (!m_enabled.load() || m_launched.load())
+ {
+ Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
+ }
+ else
+ {
+ Logger::error(L"CmdPal launch failed after {} attempts.", retry);
+ }
}
virtual bool on_hotkey(size_t) override
@@ -269,11 +339,14 @@ public:
virtual bool is_enabled() override
{
- return m_enabled;
+ return CmdPal::m_enabled.load();
}
};
+std::atomic CmdPal::m_enabled{ false };
+std::atomic CmdPal::m_launched{ false };
+
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
-}
+}
\ No newline at end of file
diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
index 782ec68bf5..18f07f5f10 100644
--- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
+++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
@@ -9,7 +9,7 @@
-
+
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs
deleted file mode 100644
index 04d474a31a..0000000000
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Globalization;
-using System.Text;
-using Microsoft.CmdPal.Ext.Calc.Properties;
-using Microsoft.CommandPalette.Extensions;
-using Microsoft.CommandPalette.Extensions.Toolkit;
-using Windows.Foundation;
-
-namespace Microsoft.CmdPal.Ext.Calc;
-
-public partial class CalculatorCommandProvider : CommandProvider
-{
- private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle };
- private readonly FallbackCalculatorItem _fallback = new();
-
- public CalculatorCommandProvider()
- {
- Id = "Calculator";
- DisplayName = Resources.calculator_display_name;
- Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
- }
-
- public override ICommandItem[] TopLevelCommands() => [_listItem];
-
- public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
-}
-
-// The calculator page is a dynamic list page
-// * The first command is where we display the results. Title=result, Subtitle=query
-// - The default command is `SaveCommand`.
-// - When you save, insert into list at spot 1
-// - change SearchText to the result
-// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
-// * The rest of the items are previously saved results
-// - Command is a CopyCommand
-// - Each item also sets the TextToSuggest to the result
-[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
-public sealed partial class CalculatorListPage : DynamicListPage
-{
- private readonly List _items = [];
- private readonly SaveCommand _saveCommand = new();
- private readonly CopyTextCommand _copyContextCommand;
- private readonly CommandContextItem _copyContextMenuItem;
- private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error);
-
- public CalculatorListPage()
- {
- Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
- Name = Resources.calculator_title;
- PlaceholderText = Resources.calculator_placeholder_text;
- Id = "com.microsoft.cmdpal.calculator";
-
- _copyContextCommand = new CopyTextCommand(string.Empty);
- _copyContextMenuItem = new CommandContextItem(_copyContextCommand);
-
- _items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") });
-
- UpdateSearchText(string.Empty, string.Empty);
-
- _saveCommand.SaveRequested += HandleSave;
- }
-
- private void HandleSave(object sender, object args)
- {
- var lastResult = _items[0].Title;
- if (!string.IsNullOrEmpty(lastResult))
- {
- var li = new ListItem(new CopyTextCommand(lastResult))
- {
- Title = _items[0].Title,
- Subtitle = _items[0].Subtitle,
- TextToSuggest = lastResult,
- };
- _items.Insert(1, li);
- _items[0].Subtitle = string.Empty;
- SearchText = lastResult;
- this.RaiseItemsChanged(this._items.Count);
- }
- }
-
- public override void UpdateSearchText(string oldSearch, string newSearch)
- {
- var firstItem = _items[0];
- if (string.IsNullOrEmpty(newSearch))
- {
- firstItem.Title = Resources.calculator_placeholder_text;
- firstItem.Subtitle = string.Empty;
- firstItem.MoreCommands = [];
- }
- else
- {
- _copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty;
- firstItem.Title = result;
- firstItem.Subtitle = newSearch;
- firstItem.MoreCommands = [_copyContextMenuItem];
- }
- }
-
- internal static bool ParseQuery(string equation, out string result)
- {
- try
- {
- var resultNumber = new DataTable().Compute(equation, null);
- result = resultNumber.ToString() ?? string.Empty;
- return true;
- }
- catch (Exception e)
- {
- result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message);
- return false;
- }
- }
-
- public override IListItem[] GetItems() => _items.ToArray();
-}
-
-[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
-public sealed partial class SaveCommand : InvokableCommand
-{
- public event TypedEventHandler
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
index 576ea08140..6d59aa66b4 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
-public class ExtensionService : IExtensionService, IDisposable
+public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler>? OnExtensionAdded;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
index ed15268507..83644c8d44 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
@@ -12,6 +12,7 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
+// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
- var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
- if (hr.Value == -2147024893)
+ unsafe
{
- Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
+ var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
- // We don't really need to throw this exception.
- // We'll just return out nothing.
- return;
+ if (hr.Value == -2147024893)
+ {
+ Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
+
+ // We don't really need to throw this exception.
+ // We'll just return out nothing.
+ return;
+ }
+
+ extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
+ if (hr < 0)
+ {
+ Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
+ Marshal.ThrowExceptionForHR(hr);
+ }
+
+ // extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
+ extensionPtr = (nint)extensionObj;
+ if (hr < 0)
+ {
+ Marshal.ThrowExceptionForHR(hr);
+ }
+
+ _extensionObject = MarshalInterface.FromAbi(extensionPtr);
}
-
- extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
- if (hr < 0)
- {
- Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
- Marshal.ThrowExceptionForHR(hr);
- }
-
- _extensionObject = MarshalInterface.FromAbi(extensionPtr);
}
finally
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json
new file mode 100644
index 0000000000..59fa7259c4
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "allowMarshaling": false
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
index a082f0acd2..126efa83ca 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
- private Task InitializeAsync()
+ internal Task InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
+ var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
case nameof(Icon):
this.Icon = new(model.Icon);
break;
+ default:
+ updateProperty = false;
+ break;
}
- UpdateProperty(propertyName);
+ // GH #38829: If we always UpdateProperty here, then there's a possible
+ // race condition, where we raise the PropertyChanged(SearchText)
+ // before the subclass actually retrieves the new SearchText from the
+ // model. In that race situation, if the UI thread handles the
+ // PropertyChanged before ListViewModel fetches the SearchText, it'll
+ // think that the old search text is the _new_ value.
+ if (updateProperty)
+ {
+ UpdateProperty(propertyName);
+ }
}
public new void ShowException(Exception ex, string? extensionHint = null)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
index 9e971ae510..c740341c7a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
- private List History { get; set; } = [];
+ internal List History { get; set; } = [];
public RecentCommandsManager()
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
index 785c27c3c9..ae97849f7a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -26,6 +26,8 @@ public partial class SettingsModel : ObservableObject
public HotkeySettings? Hotkey { get; set; } = DefaultActivationShortcut;
+ public bool UseLowLevelGlobalHotkey { get; set; }
+
public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
@@ -36,6 +38,10 @@ public partial class SettingsModel : ObservableObject
public bool HighlightSearchOnActivate { get; set; } = true;
+ public bool ShowSystemTrayIcon { get; set; } = true;
+
+ public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
+
public Dictionary ProviderSettings { get; set; } = [];
public Dictionary Aliases { get; set; } = [];
@@ -87,7 +93,7 @@ public partial class SettingsModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
- var loaded = JsonSerializer.Deserialize(jsonContent, _deserializerOptions);
+ var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -111,7 +117,7 @@ public partial class SettingsModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
- var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
+ var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -127,7 +133,7 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
- var serialized = savedSettings.ToJsonString(_serializerOptions);
+ var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -160,19 +166,34 @@ public partial class SettingsModel : ObservableObject
return Path.Combine(directory, "settings.json");
}
- private static readonly JsonSerializerOptions _serializerOptions = new()
- {
- WriteIndented = true,
- Converters = { new JsonStringEnumConverter() },
- };
+ // [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
+ // private static readonly JsonSerializerOptions _serializerOptions = new()
+ // {
+ // WriteIndented = true,
+ // Converters = { new JsonStringEnumConverter() },
+ // };
+ // private static readonly JsonSerializerOptions _deserializerOptions = new()
+ // {
+ // PropertyNameCaseInsensitive = true,
+ // IncludeFields = true,
+ // Converters = { new JsonStringEnumConverter() },
+ // AllowTrailingCommas = true,
+ // };
+}
- private static readonly JsonSerializerOptions _deserializerOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- IncludeFields = true,
- Converters = { new JsonStringEnumConverter() },
- AllowTrailingCommas = true,
- };
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(HistoryItem))]
+[JsonSerializable(typeof(SettingsModel))]
+[JsonSerializable(typeof(AppStateModel))]
+[JsonSerializable(typeof(List), TypeInfoPropertyName = "HistoryList")]
+[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
+internal sealed partial class JsonSerializationContext : JsonSerializerContext
+{
}
public enum MonitorBehavior
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index 190d75b0c3..2083d35191 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -27,6 +27,17 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
+ public bool UseLowLevelGlobalHotkey
+ {
+ get => _settings.UseLowLevelGlobalHotkey;
+ set
+ {
+ _settings.UseLowLevelGlobalHotkey = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Hotkey)));
+ Save();
+ }
+ }
+
public bool ShowAppDetails
{
get => _settings.ShowAppDetails;
@@ -87,6 +98,26 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
+ public bool ShowSystemTrayIcon
+ {
+ get => _settings.ShowSystemTrayIcon;
+ set
+ {
+ _settings.ShowSystemTrayIcon = value;
+ Save();
+ }
+ }
+
+ public bool IgnoreShortcutWhenFullscreen
+ {
+ get => _settings.IgnoreShortcutWhenFullscreen;
+ set
+ {
+ _settings.IgnoreShortcutWhenFullscreen = value;
+ Save();
+ }
+ }
+
public ObservableCollection CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
index 043598196b..d86831d0a1 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
- () =>
+ async () =>
{
- var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ // bool f = await viewModel.InitializeCommand.ExecutionTask.;
+ // var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ // var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ var result = await viewModel.InitializeAsync();
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
index 4b8a2b49a9..5ff8337494 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs
@@ -152,7 +152,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
GenerateId();
- UpdateAlias();
+ FetchAliasFromAliasManager();
UpdateHotkey();
UpdateTags();
}
@@ -163,24 +163,31 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private void HandleChangeAlias()
{
- SetAlias(Alias);
+ SetAlias();
Save();
}
- public void SetAlias(CommandAlias? newAlias)
+ public void SetAlias()
{
- _serviceProvider.GetService()!.UpdateAlias(Id, newAlias);
- UpdateAlias();
+ var commandAlias = Alias is null
+ ? null
+ : new CommandAlias(Alias.Alias, Alias.CommandId, Alias.IsDirect);
+
+ _serviceProvider.GetService()!.UpdateAlias(Id, commandAlias);
UpdateTags();
}
- private void UpdateAlias()
+ private void FetchAliasFromAliasManager()
{
- // Add tags for the alias, if we have one.
- var aliases = _serviceProvider.GetService();
- if (aliases != null)
+ var am = _serviceProvider.GetService();
+ if (am != null)
{
- Alias = aliases.AliasFromId(Id);
+ var commandAlias = am.AliasFromId(Id);
+ if (commandAlias is not null)
+ {
+ // Decouple from the alias manager alias object
+ Alias = new CommandAlias(commandAlias.Alias, commandAlias.CommandId, commandAlias.IsDirect);
+ }
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index 9f7b4d4071..203009f763 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -36,7 +36,7 @@
-
+
@@ -53,37 +53,16 @@
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}" />
-
+ Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
-
-
-
-
-
-
-
-
-
@@ -92,9 +71,9 @@
Padding="4"
ColumnSpacing="8">
-
+
@@ -157,6 +133,8 @@
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
+ TextTrimming="CharacterEllipsis"
+ TextWrapping="NoWrap"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
@@ -245,35 +219,48 @@
x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton"
Padding="4"
- animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
- animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
- ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Content="{ui:FontIcon Glyph=,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
- ToolTipService.ToolTip="Ctrl+k"
+ ToolTipService.ToolTip="Ctrl+K"
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
index 382e7ba7ea..f079f4b513 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
@@ -6,18 +6,22 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
+using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
+using Windows.System;
+using Windows.UI.Core;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient,
+ IRecipient,
ICurrentPageAware
{
- public CommandBarViewModel ViewModel { get; set; } = new();
+ public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
@@ -35,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
+
+ ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -49,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
- CommandsDropdown.SelectedIndex = 0;
- CommandsDropdown.Focus(FocusState.Programmatic);
+ UpdateUiForStackChange();
+ }
+
+ public void Receive(TryCommandKeybindingMessage msg)
+ {
+ if (!ViewModel.ShouldShowContextMenu)
+ {
+ return;
+ }
+
+ var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
+
+ if (result == ContextKeybindingResult.Hide)
+ {
+ msg.Handled = true;
+ }
+ else if (result == ContextKeybindingResult.KeepOpen)
+ {
+ if (!MoreCommandsButton.Flyout.IsOpen)
+ {
+ var options = new FlyoutShowOptions
+ {
+ ShowMode = FlyoutShowMode.Standard,
+ };
+ MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
+ }
+
+ UpdateUiForStackChange();
+
+ msg.Handled = true;
+ }
+ else if (result == ContextKeybindingResult.Unhandled)
+ {
+ msg.Handled = false;
+ }
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
@@ -85,8 +125,160 @@ public sealed partial class CommandBar : UserControl,
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
- ViewModel?.InvokeItemCommand.Execute(item);
- MoreCommandsButton.Flyout.Hide();
+ if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
+ {
+ MoreCommandsButton.Flyout.Hide();
+ }
+ else
+ {
+ UpdateUiForStackChange();
+ }
}
}
+
+ private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Handled)
+ {
+ return;
+ }
+
+ var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
+ InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
+
+ var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
+
+ if (result == ContextKeybindingResult.Hide)
+ {
+ e.Handled = true;
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+ else if (result == ContextKeybindingResult.KeepOpen)
+ {
+ e.Handled = true;
+ }
+ else if (result == ContextKeybindingResult.Unhandled)
+ {
+ e.Handled = false;
+ }
+ }
+
+ private void Flyout_Opened(object sender, object e)
+ {
+ UpdateUiForStackChange();
+ }
+
+ private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
+ {
+ ViewModel?.ClearContextStack();
+ WeakReferenceMessenger.Default.Send();
+ }
+
+ private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ var prop = e.PropertyName;
+ if (prop == nameof(ViewModel.ContextMenu))
+ {
+ UpdateUiForStackChange();
+ }
+ }
+
+ private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
+
+ if (CommandsDropdown.SelectedIndex == -1)
+ {
+ CommandsDropdown.SelectedIndex = 0;
+ }
+ }
+
+ private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
+ InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
+
+ if (e.Key == VirtualKey.Enter)
+ {
+ if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
+ {
+ if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
+ {
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+ else
+ {
+ UpdateUiForStackChange();
+ }
+
+ e.Handled = true;
+ }
+ }
+ else if (e.Key == VirtualKey.Escape ||
+ (e.Key == VirtualKey.Left && altPressed))
+ {
+ if (ViewModel.CanPopContextStack())
+ {
+ ViewModel.PopContextStack();
+ UpdateUiForStackChange();
+ }
+ else
+ {
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+
+ e.Handled = true;
+ }
+
+ CommandsDropdown_KeyDown(sender, e);
+ }
+
+ private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Up)
+ {
+ // navigate previous
+ if (CommandsDropdown.SelectedIndex > 0)
+ {
+ CommandsDropdown.SelectedIndex--;
+ }
+ else
+ {
+ CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
+ }
+
+ e.Handled = true;
+ }
+ else if (e.Key == VirtualKey.Down)
+ {
+ // navigate next
+ if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
+ {
+ CommandsDropdown.SelectedIndex++;
+ }
+ else
+ {
+ CommandsDropdown.SelectedIndex = 0;
+ }
+
+ e.Handled = true;
+ }
+ }
+
+ private void UpdateUiForStackChange()
+ {
+ ContextFilterBox.Text = string.Empty;
+ ViewModel.ContextMenu?.SetSearchText(string.Empty);
+ CommandsDropdown.SelectedIndex = 0;
+ ContextFilterBox.Focus(FocusState.Programmatic);
+ }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
index f41ac98ea3..c868e3dd5e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
@@ -105,7 +105,9 @@ public sealed partial class SearchBar : UserControl,
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
-
+ var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
+ InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ctrlPressed && e.Key == VirtualKey.Enter)
{
// ctrl+enter
@@ -164,6 +166,16 @@ public sealed partial class SearchBar : UserControl,
{
WeakReferenceMessenger.Default.Send(new());
}
+
+ if (!e.Handled)
+ {
+ // The CommandBar is responsible for handling all the item keybindings,
+ // since the bound context item may need to then show another
+ // context menu
+ TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
+ WeakReferenceMessenger.Default.Send(msg);
+ e.Handled = msg.Handled;
+ }
}
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
@@ -250,23 +262,36 @@ public sealed partial class SearchBar : UserControl,
private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var property = e.PropertyName;
- if (CurrentPageViewModel is ListViewModel list &&
- property == nameof(ListViewModel.SearchText))
- {
- // Only if the text actually changed...
- // (sometimes this triggers on a round-trip of the SearchText)
- if (FilterBox.Text != list.SearchText)
- {
- // ... Update our displayed text, and...
- FilterBox.Text = list.SearchText;
- // ... Move the cursor to the end of the input
- FilterBox.Select(FilterBox.Text.Length, 0);
+ if (CurrentPageViewModel is ListViewModel list)
+ {
+ if (property == nameof(ListViewModel.SearchText))
+ {
+ // Only if the text actually changed...
+ // (sometimes this triggers on a round-trip of the SearchText)
+ if (FilterBox.Text != list.SearchText)
+ {
+ // ... Update our displayed text, and...
+ FilterBox.Text = list.SearchText;
+
+ // ... Move the cursor to the end of the input
+ FilterBox.Select(FilterBox.Text.Length, 0);
+ }
+ }
+ else if (property == nameof(ListViewModel.InitialSearchText))
+ {
+ // GH #38712:
+ // The ListPage will notify us of the `InitialSearchText` when
+ // we first load the viewmodel. We can use that as an
+ // opportunity to immediately select the search text. That lets
+ // the user start typing a new search without manually
+ // selecting the old one.
+ SelectSearch();
}
}
}
public void Receive(GoHomeMessage message) => ClearSearch();
- public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
+ public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml
index 54f3c69b23..1a522ebdc6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/Tag.xaml
@@ -38,6 +38,7 @@
+
@@ -56,6 +57,7 @@
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
+ AutomationProperties.Name="{TemplateBinding Text}"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
index 395bd0ebe6..060b382f28 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
@@ -28,21 +28,22 @@
NotEmptyValue="Visible" />
-
-
-
+
-
-
+
@@ -55,6 +56,7 @@
Width="20"
Height="20"
Margin="4,0,4,0"
+ AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
@@ -136,14 +138,15 @@
+ Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
+ Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..a3227ca77c
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+using System.Security;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+[SuppressUnmanagedCodeSecurity]
+internal static class NativeMethods
+{
+ [DllImport("shell32.dll")]
+ public static extern int SHQueryUserNotificationState(out UserNotificationState state);
+}
+
+internal enum UserNotificationState : int
+{
+ QUNS_NOT_PRESENT = 1,
+ QUNS_BUSY,
+ QUNS_RUNNING_D3D_FULL_SCREEN,
+ QUNS_PRESENTATION_MODE,
+ QUNS_ACCEPTS_NOTIFICATIONS,
+ QUNS_QUIET_TIME,
+ QUNS_APP,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs
new file mode 100644
index 0000000000..c0d257088e
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+internal sealed partial class WindowHelper
+{
+ public static bool IsWindowFullscreen()
+ {
+ UserNotificationState state;
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
+ if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) == null)
+ {
+ if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
+ state == UserNotificationState.QUNS_BUSY ||
+ state == UserNotificationState.QUNS_PRESENTATION_MODE)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
index ac1b0e0860..ce20c9be77 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
+using CmdPalKeyboardService;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Messages;
@@ -42,6 +43,7 @@ public sealed partial class MainWindow : Window,
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List _hotkeys = [];
+ private bool _ignoreHotKeyWhenFullScreen = true;
// Stylistically, window messages are WM_*
#pragma warning disable SA1310 // Field names should not contain underscore
@@ -52,9 +54,10 @@ public sealed partial class MainWindow : Window,
#pragma warning restore SA1306 // Field names should begin with lower-case letter
#pragma warning restore SA1310 // Field names should not contain underscore
+ private readonly KeyboardListener _keyboardListener;
+
// Notification Area ("Tray") icon data
private NOTIFYICONDATAW? _trayIconData;
- private bool _createdIcon;
private DestroyIconSafeHandle? _largeIcon;
private DesktopAcrylicController? _acrylicController;
@@ -67,6 +70,11 @@ public sealed partial class MainWindow : Window,
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
+ _keyboardListener = new KeyboardListener();
+ _keyboardListener.Start();
+
+ _keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
+
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
// notification area icon back
@@ -99,7 +107,6 @@ public sealed partial class MainWindow : Window,
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
- AddNotificationIcon();
// Load our settings, and then also wire up a settings changed handler
HotReloadSettings();
@@ -149,6 +156,9 @@ public sealed partial class MainWindow : Window,
var settings = App.Current.Services.GetService()!;
SetupHotkey(settings);
+ SetupTrayIcon(settings.ShowSystemTrayIcon);
+
+ _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
// This will prevent our window from appearing in alt+tab or the taskbar.
// You'll _need_ to use the hotkey to summon it.
@@ -279,16 +289,12 @@ public sealed partial class MainWindow : Window,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
- public void Receive(HideWindowMessage message)
- {
- PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
- }
+ public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
+
+ public void Receive(QuitMessage message) =>
- public void Receive(QuitMessage message)
- {
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
- }
public void Receive(DismissMessage message) =>
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
@@ -299,12 +305,14 @@ public sealed partial class MainWindow : Window,
var extensionService = serviceProvider.GetService()!;
extensionService.SignalStopExtensionsAsync();
- RemoveNotificationIcon();
+ RemoveTrayIcon();
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
DisposeAcrylic();
+
+ _keyboardListener.Stop();
}
private void DisposeAcrylic()
@@ -369,12 +377,17 @@ public sealed partial class MainWindow : Window,
// ... then don't hide the window when it loses focus.
return;
}
- else
- {
- PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
- PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
+ // Are we disabled? If we are, then we don't want to dismiss on focus lost.
+ // This can happen if an extension wanted to show a modal dialog on top of our
+ // window i.e. in the case of an MSAL auth window.
+ if (PInvoke.IsWindowEnabled(_hwnd) == 0)
+ {
+ return;
}
+
+ PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
+ PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
if (_configurationSource != null)
@@ -383,14 +396,13 @@ public sealed partial class MainWindow : Window,
}
}
- public void Summon(string commandId)
- {
+ public void Summon(string commandId) =>
+
// The actual showing and hiding of the window will be done by the
// ShellPage. This is because we don't want to show the window if the
// user bound a hotkey to just an invokable command, which we can't
// know till the message is being handled.
WeakReferenceMessenger.Default.Send(new(commandId, _hwnd));
- }
#pragma warning disable SA1310 // Field names should not contain underscore
private const uint DOT_KEY = 0xBE;
@@ -399,6 +411,8 @@ public sealed partial class MainWindow : Window,
private void UnregisterHotkeys()
{
+ _keyboardListener.ClearHotkeys();
+
while (_hotkeys.Count > 0)
{
PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1);
@@ -413,19 +427,28 @@ public sealed partial class MainWindow : Window,
var globalHotkey = settings.Hotkey;
if (globalHotkey != null)
{
- var vk = globalHotkey.Code;
- var modifiers =
- (globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
- (globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
- (globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
- (globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
- ;
-
- var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
- if (success)
+ if (settings.UseLowLevelGlobalHotkey)
{
+ _keyboardListener.SetHotkeyAction(globalHotkey.Win, globalHotkey.Ctrl, globalHotkey.Shift, globalHotkey.Alt, (byte)globalHotkey.Code, string.Empty);
+
_hotkeys.Add(new(globalHotkey, string.Empty));
}
+ else
+ {
+ var vk = globalHotkey.Code;
+ var modifiers =
+ (globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
+ (globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
+ (globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
+ (globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
+ ;
+
+ var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
+ if (success)
+ {
+ _hotkeys.Add(new(globalHotkey, string.Empty));
+ }
+ }
}
foreach (var commandHotkey in settings.CommandHotkeys)
@@ -451,6 +474,26 @@ public sealed partial class MainWindow : Window,
}
}
+ private void HandleSummon(string commandId)
+ {
+ var isRootHotkey = string.IsNullOrEmpty(commandId);
+ PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
+
+ // Note to future us: the wParam will have the index of the hotkey we registered.
+ // We can use that in the future to differentiate the hotkeys we've pressed
+ // so that we can bind hotkeys to individual commands
+ if (!this.Visible || !isRootHotkey)
+ {
+ Activate();
+
+ Summon(commandId);
+ }
+ else if (isRootHotkey)
+ {
+ PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
+ }
+ }
+
private LRESULT HotKeyPrc(
HWND hwnd,
uint uMsg,
@@ -464,23 +507,33 @@ public sealed partial class MainWindow : Window,
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
{
+ if (_ignoreHotKeyWhenFullScreen)
+ {
+ // If we're in full screen mode, ignore the hotkey
+ if (WindowHelper.IsWindowFullscreen())
+ {
+ return (LRESULT)IntPtr.Zero;
+ }
+ }
+
var hotkey = _hotkeys[hotkeyIndex];
- var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
- PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
+ HandleSummon(hotkey.CommandId);
- // Note to future us: the wParam will have the index of the hotkey we registered.
- // We can use that in the future to differentiate the hotkeys we've pressed
- // so that we can bind hotkeys to individual commands
- if (!this.Visible || !isRootHotkey)
- {
- Activate();
+ // var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
- Summon(hotkey.CommandId);
- }
- else if (isRootHotkey)
- {
- PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
- }
+ // // Note to future us: the wParam will have the index of the hotkey we registered.
+ // // We can use that in the future to differentiate the hotkeys we've pressed
+ // // so that we can bind hotkeys to individual commands
+ // if (!this.Visible || !isRootHotkey)
+ // {
+ // Activate();
+
+ // Summon(hotkey.CommandId);
+ // }
+ // else if (isRootHotkey)
+ // {
+ // PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
+ // }
}
return (LRESULT)IntPtr.Zero;
@@ -491,9 +544,9 @@ public sealed partial class MainWindow : Window,
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
case PInvoke.WM_WINDOWPOSCHANGING:
{
- if (!_createdIcon)
+ if (_trayIconData == null)
{
- AddNotificationIcon();
+ SetupTrayIcon();
}
}
@@ -505,7 +558,7 @@ public sealed partial class MainWindow : Window,
{
// Handle the case where explorer.exe restarts.
// Even if we created it before, do it again
- AddNotificationIcon();
+ SetupTrayIcon();
}
else if (uMsg == WM_TRAY_ICON)
{
@@ -525,55 +578,60 @@ public sealed partial class MainWindow : Window,
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
- private void AddNotificationIcon()
+ private void SetupTrayIcon(bool? showSystemTrayIcon = null)
{
- // We only need to build the tray data once.
- if (_trayIconData == null)
+ if (showSystemTrayIcon ?? App.Current.Services.GetService()!.ShowSystemTrayIcon)
{
- // We need to stash this handle, so it doesn't clean itself up. If
- // explorer restarts, we'll come back through here, and we don't
- // really need to re-load the icon in that case. We can just use
- // the handle from the first time.
- _largeIcon = GetAppIconHandle();
- _trayIconData = new NOTIFYICONDATAW()
+ // We only need to build the tray data once.
+ if (_trayIconData == null)
{
- cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATAW)),
- hWnd = _hwnd,
- uID = MY_NOTIFY_ID,
- uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
- uCallbackMessage = WM_TRAY_ICON,
- hIcon = (HICON)_largeIcon.DangerousGetHandle(),
- szTip = RS_.GetString("AppStoreName"),
- };
+ // We need to stash this handle, so it doesn't clean itself up. If
+ // explorer restarts, we'll come back through here, and we don't
+ // really need to re-load the icon in that case. We can just use
+ // the handle from the first time.
+ _largeIcon = GetAppIconHandle();
+ _trayIconData = new NOTIFYICONDATAW()
+ {
+ cbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATAW)),
+ hWnd = _hwnd,
+ uID = MY_NOTIFY_ID,
+ uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
+ uCallbackMessage = WM_TRAY_ICON,
+ hIcon = (HICON)_largeIcon.DangerousGetHandle(),
+ szTip = RS_.GetString("AppStoreName"),
+ };
+ }
+
+ var d = (NOTIFYICONDATAW)_trayIconData;
+
+ // Add the notification icon
+ PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d);
}
-
- var d = (NOTIFYICONDATAW)_trayIconData;
-
- // Add the notification icon
- if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_ADD, in d))
+ else
{
- _createdIcon = true;
+ RemoveTrayIcon();
}
}
- private void RemoveNotificationIcon()
+ private void RemoveTrayIcon()
{
- if (_trayIconData != null && _createdIcon)
+ if (_trayIconData != null)
{
var d = (NOTIFYICONDATAW)_trayIconData;
if (PInvoke.Shell_NotifyIcon(NOTIFY_ICON_MESSAGE.NIM_DELETE, in d))
{
- _createdIcon = false;
+ _trayIconData = null;
}
}
+
+ _largeIcon?.Close();
}
private DestroyIconSafeHandle GetAppIconHandle()
{
var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
DestroyIconSafeHandle largeIcon;
- DestroyIconSafeHandle smallIcon;
- PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out smallIcon, 1);
+ PInvoke.ExtractIconEx(exePath, 0, out largeIcon, out _, 1);
return largeIcon;
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index e1f891558d..c79669e714 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -23,13 +23,13 @@
false
-
+
true
- Microsoft.Terminal.UI
+ Microsoft.Terminal.UI;CmdPalKeyboardService
$(OutDir)
@@ -73,12 +73,17 @@
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
index 03e8c40eaa..6c63eeff16 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
@@ -322,6 +322,18 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
This key will open the Command Palette.
+
+ Use low-level keyboard hook
+
+
+ Try this if there are issues with the shortcut (Command Palette might not get focus when triggered from an elevated window)
+
+
+ Ignore shortcut in fullscreen mode
+
+
+ Preventing disruption of the program running in fullscreen by unintentional activation of shortcut
+
Go home when activated
@@ -373,9 +385,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Extensions
-
- More commands
-
Open Command Palette settings
@@ -385,4 +394,22 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Behavior
+
+ Search commands...
+
+
+ Show system tray icon
+
+
+ Choose if Command Palette is visible in the system tray
+
+
+ Back
+
+
+ Back
+
+
+ More
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj b/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
index 039bf41817..69e3c64635 100644
--- a/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
+++ b/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
@@ -3,7 +3,7 @@
..\..\..\..\
- $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.6.250205002
+ $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001
diff --git a/src/modules/cmdpal/README.md b/src/modules/cmdpal/README.md
index 83f37f0c16..2ca4f2f0cf 100644
--- a/src/modules/cmdpal/README.md
+++ b/src/modules/cmdpal/README.md
@@ -40,7 +40,7 @@ Projects of interest are:
[Initial SDK Spec]: ./doc/initial-sdk-spec/initial-sdk-spec.md
[generic samples]: ./Exts/SamplePagesExtension
-[real samples]: .Exts/ProcessMonitorExtension
+[real samples]: ./Exts/ProcessMonitorExtension
[real extensions that we've "shipped" already]: https://github.com/zadjii/CmdPalExtensions/blob/main/src/extensions
diff --git a/src/modules/cmdpal/doc/command-pal-anatomy/command-palette-anatomy.md b/src/modules/cmdpal/doc/command-pal-anatomy/command-palette-anatomy.md
index 2af4771e16..878daaa69a 100644
--- a/src/modules/cmdpal/doc/command-pal-anatomy/command-palette-anatomy.md
+++ b/src/modules/cmdpal/doc/command-pal-anatomy/command-palette-anatomy.md
@@ -152,7 +152,7 @@ We've made it easy to build a new extension. Just follow these steps:
2. Run the following PowerShell script, replacing "MastodonExtension" with the `Name` of your extension and "Mastodon extension for cmdpal" with the `DisplayName` of the [command that will show up in the root view](#root-view) of the Command Palette:
```powershell
-.\Exts\NewExtension.ps1 -name MastodonExtension -DisplayName "Mastodon extension for cmdpal"
+.\ext\NewExtension.ps1 -name MastodonExtension -DisplayName "Mastodon extension for cmdpal"
```
3. Open the solution in Visual Studio.
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
similarity index 80%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
index ed724794fd..f271d4d556 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
@@ -57,7 +57,7 @@ public sealed partial class AllAppsPage : ListPage
Stopwatch stopwatch = new();
stopwatch.Start();
- List apps = GetPrograms();
+ var apps = GetPrograms();
this.allAppsSection = apps
.Select((app) => new AppListItem(app, true))
@@ -73,26 +73,15 @@ public sealed partial class AllAppsPage : ListPage
internal List GetPrograms()
{
- IEnumerable uwpResults = AppCache.Instance.Value.UWPs
+ var uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
- .Select(app =>
- new AppItem()
- {
- Name = app.Name,
- Subtitle = app.Description,
- Type = UWPApplication.Type(),
- IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty,
- DirPath = app.Location,
- UserModelId = app.UserModelId,
- IsPackaged = true,
- Commands = app.GetCommands(),
- });
+ .Select(UwpToAppItem);
- IEnumerable win32Results = AppCache.Instance.Value.Win32s
+ var win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app =>
{
- string icoPath = string.IsNullOrEmpty(app.IcoPath) ?
+ var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
@@ -116,4 +105,21 @@ public sealed partial class AllAppsPage : ListPage
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
}
+
+ private AppItem UwpToAppItem(UWPApplication app)
+ {
+ var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
+ var item = new AppItem()
+ {
+ Name = app.Name,
+ Subtitle = app.Description,
+ Type = UWPApplication.Type(),
+ IcoPath = iconPath,
+ DirPath = app.Location,
+ UserModelId = app.UserModelId,
+ IsPackaged = true,
+ Commands = app.GetCommands(),
+ };
+ return item;
+ }
}
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppCache.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppCache.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/AppCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/AppCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCommand.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
similarity index 74%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
index 1ab8df76ef..57c9175d4d 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
@@ -32,7 +32,13 @@ internal sealed partial class AppListItem : ListItem
Tags = [_appTag];
MoreCommands = _app.Commands!.ToArray();
- _details = new Lazy(() => BuildDetails());
+ _details = new Lazy(() =>
+ {
+ var t = BuildDetails();
+ t.Wait();
+ return t.Result;
+ });
+
_icon = new Lazy(() =>
{
var t = FetchIcon(useThumbnails);
@@ -41,8 +47,9 @@ internal sealed partial class AppListItem : ListItem
});
}
- private Details BuildDetails()
+ private async Task BuildDetails()
{
+ // Build metadata, with app type, path, etc.
var metadata = new List();
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
if (!_app.IsPackaged)
@@ -50,10 +57,33 @@ internal sealed partial class AppListItem : ListItem
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
}
+ // Icon
+ IconInfo? heroImage = null;
+ if (_app.IsPackaged)
+ {
+ heroImage = new IconInfo(_app.IcoPath);
+ }
+ else
+ {
+ try
+ {
+ var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
+ if (stream != null)
+ {
+ heroImage = IconInfo.FromStream(stream);
+ }
+ }
+ catch (Exception)
+ {
+ // do nothing if we fail to load an icon.
+ // Logging it would be too NOISY, there's really no need.
+ }
+ }
+
return new Details()
{
Title = this.Title,
- HeroImage = this.Icon ?? new IconInfo(string.Empty),
+ HeroImage = heroImage ?? this.Icon ?? new IconInfo(string.Empty),
Metadata = metadata.ToArray(),
};
}
@@ -64,11 +94,6 @@ internal sealed partial class AppListItem : ListItem
if (_app.IsPackaged)
{
icon = new IconInfo(_app.IcoPath);
- if (_details.IsValueCreated)
- {
- _details.Value.HeroImage = icon;
- }
-
return icon;
}
@@ -94,11 +119,6 @@ internal sealed partial class AppListItem : ListItem
icon = new IconInfo(_app.IcoPath);
}
- if (_details.IsValueCreated)
- {
- _details.Value.HeroImage = icon;
- }
-
return icon;
}
}
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.png
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Assets/AllApps.svg
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/NativeMethods.txt
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/AppxFactory.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/AppxPackageHelper.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/DisabledProgramSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/DisabledProgramSource.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/DisabledProgramSource.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/DisabledProgramSource.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IApplicationActivationManager.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxFactory.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplication.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestApplicationsEnumerator.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestProperties.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IAppxManifestReader.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/IFileVersionInfoWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IFileVersionInfoWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/IFileVersionInfoWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IFileVersionInfoWrapper.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackage.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageCatalog.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackageCatalog.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageCatalog.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackageCatalog.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IPackageManager.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IProgram.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/LogoType.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageCatalogWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageCatalogWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageCatalogWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageCatalogWrapper.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageManagerWrapper.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/PackageWrapper.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/ProgramSource.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/ReparsePoint.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/ReparsePoint.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Programs/ReparsePoint.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/ReparsePoint.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWP.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs
similarity index 98%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs
index 6a4cf94480..7a35400a7a 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs
@@ -6,13 +6,11 @@ using System;
using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
-using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
@@ -314,7 +312,12 @@ public class UWPApplication : IProgram
}
}
- var selectedIconPath = paths.FirstOrDefault(File.Exists);
+ // By working from the highest resolution to the lowest, we make
+ // sure that we use the highest quality possible icon for the app.
+ //
+ // FirstOrDefault would result in us using the 1x scaled icon
+ // always, which is usually too small for our needs.
+ var selectedIconPath = paths.LastOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selectedIconPath))
{
LogoPath = selectedIconPath;
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/EventHandler.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/EventHandler.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/EventHandler.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/EventHandler.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/FileSystemWatcherWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/FileSystemWatcherWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/FileSystemWatcherWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/FileSystemWatcherWrapper.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IFileSystemWatcherWrapper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IFileSystemWatcherWrapper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IFileSystemWatcherWrapper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IFileSystemWatcherWrapper.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IProgramRepository.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IProgramRepository.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IProgramRepository.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IProgramRepository.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IRepository`1.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IRepository`1.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/IRepository`1.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/IRepository`1.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/ListRepository`1.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/ListRepository`1.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/ListRepository`1.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/ListRepository`1.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramFileSystemWatchers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramFileSystemWatchers.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramFileSystemWatchers.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramFileSystemWatchers.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/Win32ProgramRepository.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/IShellLinkHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/IShellLinkHelper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/IShellLinkHelper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/IShellLinkHelper.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/Native.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/Native.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/Native.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/Native.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellCommand.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLinkHelper.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ShellLocalization.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/Theme.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/Theme.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/Theme.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/Theme.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Utils/ThemeHelper.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
similarity index 94%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
index 9b3f54a21f..5da419cd40 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
@@ -34,7 +34,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
- "value": {{JsonSerializer.Serialize(name)}},
+ "value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
},
@@ -42,7 +42,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"type": "Input.Text",
"style": "text",
"id": "bookmark",
- "value": {{JsonSerializer.Serialize(url)}},
+ "value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkPage.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.png
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.png
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.png
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.svg
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.svg
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Assets/Bookmark.svg
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs
new file mode 100644
index 0000000000..9730bf214d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(BookmarkData))]
+[JsonSerializable(typeof(Bookmarks))]
+[JsonSerializable(typeof(List), TypeInfoPropertyName = "BookmarkList")]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class BookmarkSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
similarity index 85%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
index 7c3a1dd1e0..8f2e257782 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
@@ -28,7 +28,7 @@ public sealed class Bookmarks
if (!string.IsNullOrEmpty(jsonStringReading))
{
- data = JsonSerializer.Deserialize(jsonStringReading, _jsonOptions) ?? new Bookmarks();
+ data = JsonSerializer.Deserialize(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
}
}
@@ -37,7 +37,7 @@ public sealed class Bookmarks
public static void WriteToFile(string path, Bookmarks data)
{
- var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
+ var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
similarity index 90%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
index eaa4ac6b42..40c3cca9f2 100644
--- a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.Bookmarks
enable
@@ -14,9 +15,9 @@
-
+
-
+
Resources.resx
@@ -24,7 +25,7 @@
True
-
+
PreserveNewest
@@ -39,5 +40,5 @@
PublicResXFileCodeGenerator
-
+
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/OpenInTerminalCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/OpenInTerminalCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/OpenInTerminalCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/OpenInTerminalCommand.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx
diff --git a/src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs
similarity index 100%
rename from src/modules/cmdpal/exts/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.png
diff --git a/src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg
similarity index 100%
rename from src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg
rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Assets/Calculator.svg
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs
new file mode 100644
index 0000000000..1478dbbd45
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CmdPal.Ext.Calc.Helper;
+using Microsoft.CmdPal.Ext.Calc.Pages;
+using Microsoft.CmdPal.Ext.Calc.Properties;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Calc;
+
+public partial class CalculatorCommandProvider : CommandProvider
+{
+ private readonly ListItem _listItem = new(new CalculatorListPage(settings))
+ {
+ Subtitle = Resources.calculator_top_level_subtitle,
+ MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)],
+ };
+
+ private readonly FallbackCalculatorItem _fallback = new(settings);
+ private static SettingsManager settings = new();
+
+ public CalculatorCommandProvider()
+ {
+ Id = "Calculator";
+ DisplayName = Resources.calculator_display_name;
+ Icon = CalculatorIcons.ProviderIcon;
+ Settings = settings.Settings;
+ }
+
+ public override ICommandItem[] TopLevelCommands() => [_listItem];
+
+ public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs
new file mode 100644
index 0000000000..67d8940ed4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+public static class BracketHelper
+{
+ public static bool IsBracketComplete(string query)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return true;
+ }
+
+ var valueTuples = query
+ .Select(BracketTrail)
+ .Where(r => r != default);
+
+ var trailTest = new Stack();
+
+ foreach (var (direction, type) in valueTuples)
+ {
+ switch (direction)
+ {
+ case TrailDirection.Open:
+ trailTest.Push(type);
+ break;
+ case TrailDirection.Close:
+ // Try to get item out of stack
+ if (!trailTest.TryPop(out var popped))
+ {
+ return false;
+ }
+
+ if (type != popped)
+ {
+ return false;
+ }
+
+ continue;
+ default:
+ {
+ throw new ArgumentOutOfRangeException($"Can't process value (Parameter direction: {direction})");
+ }
+ }
+ }
+
+ return trailTest.Count == 0;
+ }
+
+ private static (TrailDirection Direction, TrailType Type) BracketTrail(char @char)
+ {
+ switch (@char)
+ {
+ case '(':
+ return (TrailDirection.Open, TrailType.Round);
+ case ')':
+ return (TrailDirection.Close, TrailType.Round);
+ case '[':
+ return (TrailDirection.Open, TrailType.Bracket);
+ case ']':
+ return (TrailDirection.Close, TrailType.Bracket);
+ default:
+ return default;
+ }
+ }
+
+ private enum TrailDirection
+ {
+ None,
+ Open,
+ Close,
+ }
+
+ private enum TrailType
+ {
+ None,
+ Bracket,
+ Round,
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs
new file mode 100644
index 0000000000..0d6f9536db
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+using Mages.Core;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+public static class CalculateEngine
+{
+ private static readonly Engine _magesEngine = new Engine(new Configuration
+ {
+ Scope = new Dictionary
+ {
+ { "e", Math.E }, // e is not contained in the default mages engine
+ },
+ });
+
+ public const int RoundingDigits = 10;
+
+ public enum TrigMode
+ {
+ Radians,
+ Degrees,
+ Gradians,
+ }
+
+ ///
+ /// Interpret
+ ///
+ /// Use CultureInfo.CurrentCulture if something is user facing
+ public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error)
+ {
+ error = default;
+
+ if (!CalculateHelper.InputValid(input))
+ {
+ return default;
+ }
+
+ // check for division by zero
+ // We check if the string contains a slash followed by space (optional) and zero. Whereas the zero must not be followed by a dot, comma, 'b', 'o' or 'x' as these indicate a number with decimal digits or a binary/octal/hexadecimal value respectively. The zero must also not be followed by other digits.
+ if (new Regex("\\/\\s*0(?!(?:[,\\.0-9]|[box]0*[1-9a-f]))", RegexOptions.IgnoreCase).Match(input).Success)
+ {
+ error = Properties.Resources.calculator_division_by_zero;
+ return default;
+ }
+
+ // mages has quirky log representation
+ // mage has log == ln vs log10
+ input = input.
+ Replace("log(", "log10(", true, CultureInfo.CurrentCulture).
+ Replace("ln(", "log(", true, CultureInfo.CurrentCulture);
+
+ input = CalculateHelper.FixHumanMultiplicationExpressions(input);
+
+ // Get the user selected trigonometry unit
+ TrigMode trigMode = settings.TrigUnit;
+
+ // Modify trig functions depending on angle unit setting
+ input = CalculateHelper.UpdateTrigFunctions(input, trigMode);
+
+ // Expand conversions between trig units
+ input = CalculateHelper.ExpandTrigConversions(input, trigMode);
+
+ var result = _magesEngine.Interpret(input);
+
+ // This could happen for some incorrect queries, like pi(2)
+ if (result == null)
+ {
+ error = Properties.Resources.calculator_expression_not_complete;
+ return default;
+ }
+
+ result = TransformResult(result);
+ if (result is string)
+ {
+ error = result as string;
+ return default;
+ }
+
+ if (string.IsNullOrEmpty(result?.ToString()))
+ {
+ return default;
+ }
+
+ var decimalResult = Convert.ToDecimal(result, cultureInfo);
+ var roundedResult = Round(decimalResult);
+
+ return new CalculateResult()
+ {
+ Result = decimalResult,
+ RoundedResult = roundedResult,
+ };
+ }
+
+ public static decimal Round(decimal value)
+ {
+ return Math.Round(value, RoundingDigits, MidpointRounding.AwayFromZero);
+ }
+
+ private static dynamic TransformResult(object result)
+ {
+ if (result.ToString() == "NaN")
+ {
+ return Properties.Resources.calculator_not_a_number;
+ }
+
+ if (result is Function)
+ {
+ return Properties.Resources.calculator_expression_not_complete;
+ }
+
+ if (result is double[,])
+ {
+ // '[10,10]' is interpreted as array by mages engine
+ return Properties.Resources.calculator_double_array_returned;
+ }
+
+ return result;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs
new file mode 100644
index 0000000000..acab3d7b96
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs
@@ -0,0 +1,328 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+public static class CalculateHelper
+{
+ private static readonly Regex RegValidExpressChar = new Regex(
+ @"^(" +
+ @"%|" +
+ @"ceil\s*\(|floor\s*\(|exp\s*\(|max\s*\(|min\s*\(|abs\s*\(|log(?:2|10)?\s*\(|ln\s*\(|sqrt\s*\(|pow\s*\(|" +
+ @"factorial\s*\(|sign\s*\(|round\s*\(|rand\s*\(\)|randi\s*\([^\)]|" +
+ @"sin\s*\(|cos\s*\(|tan\s*\(|arcsin\s*\(|arccos\s*\(|arctan\s*\(|" +
+ @"sinh\s*\(|cosh\s*\(|tanh\s*\(|arsinh\s*\(|arcosh\s*\(|artanh\s*\(|" +
+ @"rad\s*\(|deg\s*\(|grad\s*\(|" + /* trigonometry unit conversion macros */
+ @"pi|" +
+ @"==|~=|&&|\|\||" +
+ @"((-?(\d+(\.\d*)?)|-?(\.\d+))[Ee](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
+ @"e|[0-9]|0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
+ @")+$",
+ RegexOptions.Compiled);
+
+ private const string DegToRad = "(pi / 180) * ";
+ private const string DegToGrad = "(10 / 9) * ";
+ private const string GradToRad = "(pi / 200) * ";
+ private const string GradToDeg = "(9 / 10) * ";
+ private const string RadToDeg = "(180 / pi) * ";
+ private const string RadToGrad = "(200 / pi) * ";
+
+ public static bool InputValid(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ throw new ArgumentNullException(paramName: nameof(input));
+ }
+
+ if (!RegValidExpressChar.IsMatch(input))
+ {
+ return false;
+ }
+
+ if (!BracketHelper.IsBracketComplete(input))
+ {
+ return false;
+ }
+
+ // If the input ends with a binary operator then it is not a valid input to mages and the Interpret function would throw an exception. Because we expect here that the user has not finished typing we block those inputs.
+ var trimmedInput = input.TrimEnd();
+ if (trimmedInput.EndsWith('+') || trimmedInput.EndsWith('-') || trimmedInput.EndsWith('*') || trimmedInput.EndsWith('|') || trimmedInput.EndsWith('\\') || trimmedInput.EndsWith('^') || trimmedInput.EndsWith('=') || trimmedInput.EndsWith('&') || trimmedInput.EndsWith('/') || trimmedInput.EndsWith('%'))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static string FixHumanMultiplicationExpressions(string input)
+ {
+ var output = CheckScientificNotation(input);
+ output = CheckNumberOrConstantThenParenthesisExpr(output);
+ output = CheckNumberOrConstantThenFunc(output);
+ output = CheckParenthesisExprThenFunc(output);
+ output = CheckParenthesisExprThenParenthesisExpr(output);
+ output = CheckNumberThenConstant(output);
+ output = CheckConstantThenConstant(output);
+ return output;
+ }
+
+ private static string CheckScientificNotation(string input)
+ {
+ /**
+ * NOTE: By the time the expression gets to us, it's already in English format.
+ *
+ * Regex explanation:
+ * (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
+ * -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
+ * -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
+ * e: Captures 'e' or 'E'
+ * (-?\d+): Captures an integer number (e.g. "-1" or "23")
+ */
+ var p = @"(-?(\d+(\.\d*)?)|-?(\.\d+))e(-?\d+)";
+ return Regex.Replace(input, p, "($1 * 10^($5))", RegexOptions.IgnoreCase);
+ }
+
+ /*
+ * num (exp)
+ * const (exp)
+ */
+ private static string CheckNumberOrConstantThenParenthesisExpr(string input)
+ {
+ var output = input;
+ do
+ {
+ input = output;
+ output = Regex.Replace(input, @"(\d+|pi|e)\s*(\()", m =>
+ {
+ if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
+ {
+ return m.Value;
+ }
+
+ return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
+ });
+ }
+ while (output != input);
+
+ return output;
+ }
+
+ /*
+ * num func
+ * const func
+ */
+ private static string CheckNumberOrConstantThenFunc(string input)
+ {
+ var output = input;
+ do
+ {
+ input = output;
+ output = Regex.Replace(input, @"(\d+|pi|e)\s*([a-zA-Z]+[0-9]*\s*\()", m =>
+ {
+ if (input[m.Index] == 'e' && input[m.Index + 1] == 'x' && input[m.Index + 2] == 'p')
+ {
+ return m.Value;
+ }
+
+ if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
+ {
+ return m.Value;
+ }
+
+ return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
+ });
+ }
+ while (output != input);
+
+ return output;
+ }
+
+ /*
+ * (exp) func
+ * func func
+ */
+ private static string CheckParenthesisExprThenFunc(string input)
+ {
+ var p = @"(\))\s*([a-zA-Z]+[0-9]*\s*\()";
+ var r = "$1 * $2";
+ return Regex.Replace(input, p, r);
+ }
+
+ /*
+ * (exp) (exp)
+ * func (exp)
+ */
+ private static string CheckParenthesisExprThenParenthesisExpr(string input)
+ {
+ var p = @"(\))\s*(\()";
+ var r = "$1 * $2";
+ return Regex.Replace(input, p, r);
+ }
+
+ /*
+ * num const
+ */
+ private static string CheckNumberThenConstant(string input)
+ {
+ var output = input;
+ do
+ {
+ input = output;
+ output = Regex.Replace(input, @"(\d+)\s*(pi|e)", m =>
+ {
+ if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
+ {
+ return m.Value;
+ }
+
+ return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
+ });
+ }
+ while (output != input);
+
+ return output;
+ }
+
+ /*
+ * const const
+ */
+ private static string CheckConstantThenConstant(string input)
+ {
+ var output = input;
+ do
+ {
+ input = output;
+ output = Regex.Replace(input, @"(pi|e)\s*(pi|e)", m =>
+ {
+ if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
+ {
+ return m.Value;
+ }
+
+ return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
+ });
+ }
+ while (output != input);
+
+ return output;
+ }
+
+ // Gets the index of the closing bracket of a function
+ private static int FindClosingBracketIndex(string input, int start)
+ {
+ var bracketCount = 0; // Set count to zero
+ for (var i = start; i < input.Length; i++)
+ {
+ if (input[i] == '(')
+ {
+ bracketCount++;
+ }
+ else if (input[i] == ')')
+ {
+ bracketCount--;
+ if (bracketCount == 0)
+ {
+ return i;
+ }
+ }
+ }
+
+ return -1; // Unmatched brackets
+ }
+
+ private static string ModifyTrigFunction(string input, string function, string modification)
+ {
+ // Get the RegEx pattern to match, depending on whether the function is inverse or normal
+ var pattern = function.StartsWith("arc", StringComparison.Ordinal) ? string.Empty : @"(?
+{
+ public decimal? Result { get; set; }
+
+ public decimal? RoundedResult { get; set; }
+
+ public bool Equals(CalculateResult other)
+ {
+ return Result == other.Result && RoundedResult == other.RoundedResult;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is CalculateResult other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Result, RoundedResult);
+ }
+
+ public static bool operator ==(CalculateResult left, CalculateResult right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(CalculateResult left, CalculateResult right)
+ {
+ return !(left == right);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs
new file mode 100644
index 0000000000..e3be5f2149
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+public static class CalculatorIcons
+{
+ public static IconInfo ResultIcon => new("\uE94E");
+
+ public static IconInfo SaveIcon => new("\uE74E");
+
+ public static IconInfo ErrorIcon => new("\uE783");
+
+ public static IconInfo ProviderIcon => IconHelpers.FromRelativePath("Assets\\Calculator.svg");
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs
new file mode 100644
index 0000000000..b3948dc854
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs
@@ -0,0 +1,53 @@
+// 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 ManagedCommon;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+internal static class ErrorHandler
+{
+ ///
+ /// Method to handles errors while calculating
+ ///
+ /// Bool to indicate if it is a fallback query.
+ /// User input as string including the action keyword.
+ /// Error message if applicable.
+ /// Exception if applicable.
+ /// List of results to show. Either an error message or an empty list.
+ /// Thrown if and are both filled with their default values.
+ internal static ListItem OnError(bool isFallbackSearch, string queryInput, string errorMessage, Exception exception = default)
+ {
+ string userMessage;
+
+ if (errorMessage != default)
+ {
+ Logger.LogError($"Failed to calculate <{queryInput}>: {errorMessage}");
+ userMessage = errorMessage;
+ }
+ else if (exception != default)
+ {
+ Logger.LogError($"Exception when query for <{queryInput}>", exception);
+ userMessage = exception.Message;
+ }
+ else
+ {
+ throw new ArgumentException("The arguments error and exception have default values. One of them has to be filled with valid error data (error message/exception)!");
+ }
+
+ return isFallbackSearch ? null : CreateErrorResult(userMessage);
+ }
+
+ private static ListItem CreateErrorResult(string errorMessage)
+ {
+ return new ListItem(new NoOpCommand())
+ {
+ Title = Properties.Resources.calculator_calculation_failed_title,
+ Subtitle = errorMessage,
+ Icon = CalculatorIcons.ErrorIcon,
+ };
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs
new file mode 100644
index 0000000000..8de77ebdae
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs
@@ -0,0 +1,144 @@
+// 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.Text;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+///
+/// Tries to convert all numbers in a text from one culture format to another.
+///
+public class NumberTranslator
+{
+ private readonly CultureInfo sourceCulture;
+ private readonly CultureInfo targetCulture;
+ private readonly Regex splitRegexForSource;
+ private readonly Regex splitRegexForTarget;
+
+ private NumberTranslator(CultureInfo sourceCulture, CultureInfo targetCulture)
+ {
+ this.sourceCulture = sourceCulture;
+ this.targetCulture = targetCulture;
+
+ splitRegexForSource = GetSplitRegex(this.sourceCulture);
+ splitRegexForTarget = GetSplitRegex(this.targetCulture);
+ }
+
+ ///
+ /// Create a new .
+ ///
+ /// source culture
+ /// target culture
+ /// Number translator for target culture
+ public static NumberTranslator Create(CultureInfo sourceCulture, CultureInfo targetCulture)
+ {
+ ArgumentNullException.ThrowIfNull(sourceCulture);
+
+ ArgumentNullException.ThrowIfNull(targetCulture);
+
+ return new NumberTranslator(sourceCulture, targetCulture);
+ }
+
+ ///
+ /// Translate from source to target culture.
+ ///
+ /// input string to translate
+ /// translated string
+ public string Translate(string input)
+ {
+ return Translate(input, sourceCulture, targetCulture, splitRegexForSource);
+ }
+
+ ///
+ /// Translate from target to source culture.
+ ///
+ /// input string to translate back to source culture
+ /// source culture string
+ public string TranslateBack(string input)
+ {
+ return Translate(input, targetCulture, sourceCulture, splitRegexForTarget);
+ }
+
+ private static string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex)
+ {
+ var outputBuilder = new StringBuilder();
+ var hexRegex = new Regex(@"(?:(0x[\da-fA-F]+))");
+
+ var hexTokens = hexRegex.Split(input);
+
+ foreach (var hexToken in hexTokens)
+ {
+ if (hexToken.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
+ {
+ // Mages engine has issues processing large hex number (larger than 7 hex digits + 0x prefix = 9 characters). So we convert it to decimal and pass it to the engine.
+ if (hexToken.Length > 9)
+ {
+ try
+ {
+ var num = Convert.ToInt64(hexToken, 16);
+ var numStr = num.ToString(cultureFrom);
+ outputBuilder.Append(numStr);
+ }
+ catch (Exception)
+ {
+ outputBuilder.Append(hexToken);
+ }
+ }
+ else
+ {
+ outputBuilder.Append(hexToken);
+ }
+
+ continue;
+ }
+
+ var tokens = splitRegex.Split(hexToken);
+ foreach (var token in tokens)
+ {
+ var leadingZeroCount = 0;
+
+ // Count leading zero characters.
+ foreach (var c in token)
+ {
+ if (c != '0')
+ {
+ break;
+ }
+
+ leadingZeroCount++;
+ }
+
+ // number is all zero characters. no need to add zero characters at the end.
+ if (token.Length == leadingZeroCount)
+ {
+ leadingZeroCount = 0;
+ }
+
+ decimal number;
+
+ outputBuilder.Append(
+ decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
+ ? (new string('0', leadingZeroCount) + number.ToString(cultureTo))
+ : token.Replace(cultureFrom.TextInfo.ListSeparator, cultureTo.TextInfo.ListSeparator));
+ }
+ }
+
+ return outputBuilder.ToString();
+ }
+
+ private static Regex GetSplitRegex(CultureInfo culture)
+ {
+ var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
+ if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator))
+ {
+ splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}";
+ }
+
+ splitPattern += ")+)";
+ return new Regex(splitPattern);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs
new file mode 100644
index 0000000000..6b1e62ae03
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs
@@ -0,0 +1,77 @@
+// 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.Text;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.Foundation;
+
+namespace Microsoft.CmdPal.Ext.Calc.Helper;
+
+public static partial class QueryHelper
+{
+ public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler