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/expect.txt b/.github/actions/spell-check/expect.txt
index ea8ffa19ad..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
@@ -197,6 +198,7 @@ CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
closesocket
+clp
CLSCTX
clsids
Clusion
@@ -518,11 +520,13 @@ FRAMECHANGED
frm
Froml
FROMTOUCH
+fsanitize
fsmgmt
FZE
gacutil
Gaeilge
Gaidhlig
+gameid
GC'ed
GCLP
gdi
@@ -629,6 +633,7 @@ HOTKEYF
hotkeys
hotlight
hotspot
+Hostx
HPAINTBUFFER
HRAWINPUT
HREDRAW
@@ -712,6 +717,7 @@ INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
+installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
@@ -1006,6 +1012,7 @@ netsh
newcolor
NEWDIALOGSTYLE
NEWFILE
+NEWFILEHEADER
newitem
newpath
newplus
@@ -1039,6 +1046,7 @@ NOINHERITLAYOUT
NOINTERFACE
NOINVERT
NOLINKINFO
+nologo
NOMCX
NOMINMAX
NOMIRRORBITMAP
@@ -1076,6 +1084,7 @@ NOTRACK
NOTSRCCOPY
NOTSRCERASE
NOTXORPEN
+notwindows
NOZORDER
NPH
npmjs
@@ -1270,6 +1279,7 @@ pstm
PStr
pstream
pstrm
+pswd
PSYSTEM
psz
ptb
@@ -1397,6 +1407,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
+sancov
SAVEFAILED
scanled
schedtasks
@@ -1415,6 +1426,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
+securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1568,6 +1580,7 @@ stdcpp
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
+steamapps
STGC
STGM
STGMEDIUM
@@ -1931,6 +1944,7 @@ WUX
Wwanpp
XAxis
xclip
+xcopy
XDocument
XElement
xfd
@@ -1969,3 +1983,11 @@ ZOOMITX
ZXk
ZXNs
zzz
+ACIE
+AOklab
+BCIE
+BOklab
+culori
+Evercoder
+LCh
+CIELCh
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 1b78856032..615b5633bf 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -220,6 +220,7 @@
"WinUI3Apps\\PowerToys.Settings.exe",
"PowerToys.CmdPalModuleInterface.dll",
+ "CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix"
],
"SigningInfo": {
@@ -330,6 +331,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/Directory.Packages.props b/Directory.Packages.props
index 4393655dab..b3b404ea51 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -45,7 +45,7 @@
-
+
@@ -55,7 +55,7 @@
-->
-
+
diff --git a/NOTICE.md b/NOTICE.md
index 92bdf5fe39..d604be7d7f 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1472,8 +1472,8 @@ 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
diff --git a/PowerToys.sln b/PowerToys.sln
index 9fca8d2a3a..201c0fbefe 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -708,6 +708,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System
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
Debug|ARM64 = Debug|ARM64
@@ -2588,6 +2590,12 @@ Global
{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
@@ -2859,6 +2867,7 @@ Global
{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/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/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/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/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/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 134249f049..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,15 +214,18 @@ public:
{
Logger::trace("CmdPal::enable()");
- m_enabled = true;
+ CmdPal::m_enabled.store(true);
- try
- {
- std::wstring packageName = L"Microsoft.CommandPalette";
-#ifdef _DEBUG
- packageName = L"Microsoft.CommandPalette.Dev";
+ 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(packageName, false).has_value())
+ {
+ try
{
Logger::info(L"CmdPal not installed. Installing...");
@@ -238,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()
@@ -258,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
@@ -273,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/Microsoft.CmdPal.Common/Contracts/IFileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs
deleted file mode 100644
index b9348eb520..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace Microsoft.CmdPal.Common.Contracts;
-
-public interface IFileService
-{
- T Read(string folderPath, string fileName);
-
- void Save(string folderPath, string fileName, T content);
-
- void Delete(string folderPath, string fileName);
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs
deleted file mode 100644
index 2350050e3e..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs
+++ /dev/null
@@ -1,16 +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.Threading.Tasks;
-
-namespace Microsoft.CmdPal.Common.Contracts;
-
-public interface ILocalSettingsService
-{
- Task HasSettingAsync(string key);
-
- Task ReadSettingAsync(string key);
-
- Task SaveSettingAsync(string key, T value);
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs
deleted file mode 100644
index a975083c7c..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs
+++ /dev/null
@@ -1,40 +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 Microsoft.CmdPal.Common.Services;
-using Microsoft.UI.Xaml;
-
-namespace Microsoft.CmdPal.Common.Extensions;
-
-///
-/// Extension class implementing extension methods for .
-///
-public static class ApplicationExtensions
-{
- ///
- /// Get registered services at the application level from anywhere in the
- /// application.
- ///
- /// Note:
- /// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
- /// "Application is a singleton that implements the static Current property
- /// to provide shared access to the Application instance for the current
- /// application. The singleton pattern ensures that state managed by
- /// Application, including shared resources and properties, is available
- /// from a single, shared location."
- ///
- /// Example of usage:
- ///
- /// Application.Current.GetService()
- ///
- ///
- /// Service type.
- /// Current application.
- /// Service reference.
- public static T GetService(this Application application)
- where T : class
- {
- return (application as IApp)!.GetService();
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs
deleted file mode 100644
index 660dcd2931..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs
+++ /dev/null
@@ -1,40 +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 Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-namespace Microsoft.CmdPal.Common.Extensions;
-
-public static class IHostExtensions
-{
- ///
- ///
- ///
- public static T CreateInstance(this IHost host, params object[] parameters)
- {
- return ActivatorUtilities.CreateInstance(host.Services, parameters);
- }
-
- ///
- /// Gets the service object for the specified type, or throws an exception
- /// if type was not registered.
- ///
- /// Service type
- /// Host object
- /// Service object
- /// Throw an exception if the specified
- /// type is not registered
- public static T GetService(this IHost host)
- where T : class
- {
- if (host.Services.GetService(typeof(T)) is not T service)
- {
- throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
- }
-
- return service;
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs
deleted file mode 100644
index d865e10bdb..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs
+++ /dev/null
@@ -1,36 +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.IO;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
-
-namespace Microsoft.CmdPal.Common.Helpers;
-
-public static class Json
-{
- public static async Task ToObjectAsync(string value)
- {
- if (typeof(T) == typeof(bool))
- {
- return (T)(object)bool.Parse(value);
- }
-
- await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
- return (await JsonSerializer.DeserializeAsync(stream))!;
- }
-
- public static async Task StringifyAsync(T value)
- {
- if (typeof(T) == typeof(bool))
- {
- return value!.ToString()!.ToLowerInvariant();
- }
-
- await using var stream = new MemoryStream();
- await JsonSerializer.SerializeAsync(stream, value);
- return Encoding.UTF8.GetString(stream.ToArray());
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
index 6ec1885a4c..2344fbb917 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Common.Helpers;
-public static class NativeEventWaiter
+public static partial class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
index 46dce07e5e..342667cf83 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.Common.Helpers;
-public static class RuntimeHelper
+public static partial class RuntimeHelper
{
public static bool IsMSIX
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
index ed698d1024..097aefdee9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.Common.Messages;
-public record HideWindowMessage()
+public partial record HideWindowMessage()
{
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
index 970df0df58..5f83ca54e1 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Common
enable
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs
deleted file mode 100644
index bae7422878..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace Microsoft.CmdPal.Common.Models;
-
-public class LocalSettingsOptions
-{
- public string? ApplicationDataFolder
- {
- get; set;
- }
-
- public string? LocalSettingsFile
- {
- get; set;
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
index 0d456bde31..996bbd7153 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
@@ -1,16 +1,7 @@
-EnableWindow
-CoCreateInstance
-FileOpenDialog
-FileSaveDialog
-IFileOpenDialog
-IFileSaveDialog
-SHCreateItemFromParsingName
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
-SHLoadIndirectString
-StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
GetDpiForWindow
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs
deleted file mode 100644
index cc6ef96098..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs
+++ /dev/null
@@ -1,48 +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.IO;
-using System.Text;
-using System.Text.Json;
-using Microsoft.CmdPal.Common.Contracts;
-
-namespace Microsoft.CmdPal.Common.Services;
-
-public class FileService : IFileService
-{
- private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
-
-#pragma warning disable CS8603 // Possible null reference return.
- public T Read(string folderPath, string fileName)
- {
- var path = Path.Combine(folderPath, fileName);
- if (File.Exists(path))
- {
- using var fileStream = File.OpenText(path);
- return JsonSerializer.Deserialize(fileStream.BaseStream);
- }
-
- return default;
- }
-#pragma warning restore CS8603 // Possible null reference return.
-
- public void Save(string folderPath, string fileName, T content)
- {
- if (!Directory.Exists(folderPath))
- {
- Directory.CreateDirectory(folderPath);
- }
-
- var fileContent = JsonSerializer.Serialize(content);
- File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
- }
-
- public void Delete(string folderPath, string fileName)
- {
- if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
- {
- File.Delete(Path.Combine(folderPath, fileName));
- }
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs
deleted file mode 100644
index 92980dfaff..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace Microsoft.CmdPal.Common.Services;
-
-///
-/// Interface for the current application singleton object exposing the API
-/// that can be accessed from anywhere in the application.
-///
-public interface IApp
-{
- ///
- /// Gets services registered at the application level.
- ///
- public T GetService()
- where T : class;
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs
deleted file mode 100644
index e4cd2a174b..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs
+++ /dev/null
@@ -1,120 +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.IO;
-using System.Threading.Tasks;
-using Microsoft.CmdPal.Common.Contracts;
-using Microsoft.CmdPal.Common.Helpers;
-using Microsoft.CmdPal.Common.Models;
-using Microsoft.Extensions.Options;
-using Windows.Storage;
-
-namespace Microsoft.CmdPal.Common.Services;
-
-public class LocalSettingsService : ILocalSettingsService
-{
- // TODO! for now, we're hardcoding the path as effectively:
- // %localappdata%\CmdPal\LocalSettings.json
- private const string DefaultApplicationDataFolder = "CmdPal";
- private const string DefaultLocalSettingsFile = "LocalSettings.json";
-
- private readonly IFileService _fileService;
- private readonly LocalSettingsOptions _options;
-
- private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- private readonly string _applicationDataFolder;
- private readonly string _localSettingsFile;
-
- private readonly bool _isMsix;
-
- private Dictionary _settings;
- private bool _isInitialized;
-
- public LocalSettingsService(IFileService fileService, IOptions options)
- {
- _isMsix = false; // RuntimeHelper.IsMSIX;
-
- _fileService = fileService;
- _options = options.Value;
-
- _applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
- _localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
-
- _settings = new Dictionary();
- }
-
- private async Task InitializeAsync()
- {
- if (!_isInitialized)
- {
- _settings = await Task.Run(() => _fileService.Read>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary();
-
- _isInitialized = true;
- }
- }
-
- public async Task HasSettingAsync(string key)
- {
- if (_isMsix)
- {
- return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
- }
- else
- {
- await InitializeAsync();
-
- if (_settings != null)
- {
- return _settings.ContainsKey(key);
- }
- }
-
- return false;
- }
-
- public async Task ReadSettingAsync(string key)
- {
- if (_isMsix)
- {
- if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
- {
- return await Json.ToObjectAsync((string)obj);
- }
- }
- else
- {
- await InitializeAsync();
-
- if (_settings != null && _settings.TryGetValue(key, out var obj))
- {
- var s = obj.ToString();
-
- if (s != null)
- {
- return await Json.ToObjectAsync(s);
- }
- }
- }
-
- return default;
- }
-
- public async Task SaveSettingAsync(string key, T value)
- {
- if (_isMsix)
- {
- ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
- }
- else
- {
- await InitializeAsync();
-
- _settings[key] = await Json.StringifyAsync(value!);
-
- await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
- }
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
index 69d38a8655..649e49fbc7 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
@@ -49,7 +49,7 @@ public partial class AppStateModel : 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.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -73,7 +73,7 @@ public partial class AppStateModel : 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.AppStateModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
- var serialized = savedSettings.ToJsonString(_serializerOptions);
+ var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
return Path.Combine(directory, "state.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,
- AllowTrailingCommas = true,
- PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
- ReadCommentHandling = JsonCommentHandling.Skip,
- };
+ // private static readonly JsonSerializerOptions _deserializerOptions = new()
+ // {
+ // PropertyNameCaseInsensitive = true,
+ // IncludeFields = true,
+ // AllowTrailingCommas = true,
+ // PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
+ // ReadCommentHandling = JsonCommentHandling.Skip,
+ // };
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
index 420f41f49f..06f55ccf02 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
@@ -4,18 +4,14 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
-using Microsoft.CommandPalette.Extensions;
-using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
- IRecipient,
- IRecipient
+ IRecipient
{
public ICommandBarContext? SelectedItem
{
@@ -29,6 +25,8 @@ public partial class CommandBarViewModel : ObservableObject,
field = value;
SetSelectedItem(value);
+
+ OnPropertyChanged(nameof(SelectedItem));
}
}
@@ -51,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
- public partial ObservableCollection ContextCommands { get; set; } = [];
+ public partial ObservableCollection ContextMenuStack { get; set; } = [];
- private Dictionary? _contextKeybindings;
+ public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register(this);
- WeakReferenceMessenger.Default.Register(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
- public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
-
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
@@ -109,53 +104,97 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
- ContextCommands = [.. SelectedItem.AllCommands];
+
+ ContextMenuStack.Clear();
+ ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
+ OnPropertyChanged(nameof(ContextMenu));
}
else
{
ShouldShowContextMenu = false;
}
+
+ OnPropertyChanged(nameof(HasSecondaryCommand));
+ OnPropertyChanged(nameof(SecondaryCommand));
+ OnPropertyChanged(nameof(ShouldShowContextMenu));
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
- [RelayCommand]
- private void InvokeItem(CommandContextItemViewModel item) =>
- WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model));
+ // [RelayCommand]
+ public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
+ PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
- if (PrimaryCommand != null)
- {
- WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
- }
+ PerformCommand(SecondaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
- if (SecondaryCommand != null)
+ PerformCommand(SecondaryCommand);
+ }
+
+ public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ {
+ var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
+ return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
+ }
+
+ private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
+ {
+ if (command == null)
{
- WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
+ return ContextKeybindingResult.Unhandled;
+ }
+
+ if (command.HasMoreCommands)
+ {
+ ContextMenuStack.Add(new ContextMenuStackViewModel(command));
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
+ return ContextKeybindingResult.KeepOpen;
+ }
+ else
+ {
+ WeakReferenceMessenger.Default.Send(new(command.Command.Model, command.Model));
+ return ContextKeybindingResult.Hide;
}
}
- public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ public bool CanPopContextStack()
{
- if (_contextKeybindings != null)
+ return ContextMenuStack.Count > 1;
+ }
+
+ public void PopContextStack()
+ {
+ if (ContextMenuStack.Count > 1)
{
- // Does the pressed key match any of the keybindings?
- var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
- if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
- {
- // TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
- // so that the correct item is activated.
- WeakReferenceMessenger.Default.Send(new(item));
- return true;
- }
+ ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
- return false;
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
+ }
+
+ public void ClearContextStack()
+ {
+ while (ContextMenuStack.Count > 1)
+ {
+ ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
+ }
+
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
}
}
+
+public enum ContextKeybindingResult
+{
+ Unhandled,
+ Hide,
+ KeepOpen,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
index 8634b63278..24dd9e1788 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public List MoreCommands { get; private set; } = [];
- IEnumerable ICommandBarContext.MoreCommands => MoreCommands;
+ IEnumerable IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
- contextItem.InitializeProperties();
+ contextItem.SlowInitializeProperties();
});
- _defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
+ if (!string.IsNullOrEmpty(model.Command.Name))
{
- _itemTitle = Name,
- Subtitle = Subtitle,
- Command = Command,
+ _defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
+ {
+ _itemTitle = Name,
+ Subtitle = Subtitle,
+ Command = Command,
- // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
- };
+ // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
+ };
- // Only set the icon on the context item for us if our command didn't
- // have its own icon
- if (!Command.HasIcon)
- {
- _defaultCommandContextItem._listItemIcon = _listItemIcon;
+ // Only set the icon on the context item for us if our command didn't
+ // have its own icon
+ if (!Command.HasIcon)
+ {
+ _defaultCommandContextItem._listItemIcon = _listItemIcon;
+ }
}
Initialized |= InitializedState.SelectionInitialized;
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
-
- ///
- /// Generates a mapping of key -> command item for this particular item's
- /// MoreCommands. (This won't include the primary Command, but it will
- /// include the secondary one). This map can be used to quickly check if a
- /// shortcut key was pressed
- ///
- /// a dictionary of KeyChord -> Context commands, for all commands
- /// that have a shortcut key set.
- internal Dictionary Keybindings()
- {
- return MoreCommands
- .Where(c => c.HasRequestedShortcut)
- .ToDictionary(
- c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
- c => c);
- }
}
[Flags]
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
index 1939162662..44bcb49cb3 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
{
public CreatedExtensionForm(string name, string displayName, string path)
{
+ var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
TemplateJson = CardTemplate;
DataJson = $$"""
{
- "name": {{JsonSerializer.Serialize(name)}},
- "directory": {{JsonSerializer.Serialize(path)}},
- "displayName": {{JsonSerializer.Serialize(displayName)}}
+ "name": {{serializeString(name)}},
+ "directory": {{serializeString(path)}},
+ "displayName": {{serializeString(displayName)}}
}
""";
_name = name;
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
- JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
+ var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
{
return CommandResult.KeepOpen();
}
- string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
+ var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
return verb switch
{
"sln" => OpenSolution(),
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenSolution()
{
string[] parts = [_path, _name, $"{_name}.sln"];
- string pathToSolution = Path.Combine(parts);
+ var pathToSolution = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToSolution);
return CommandResult.Hide();
}
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenDirectory()
{
string[] parts = [_path, _name];
- string pathToDir = Path.Combine(parts);
+ var pathToDir = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToDir);
return CommandResult.Hide();
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
index 698faf0335..aca45f3494 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
Directory.Delete(tempDir, true);
}
- private string FormatJsonString(string str)
- {
+ private string FormatJsonString(string str) =>
+
// Escape the string for JSON
- return JsonSerializer.Serialize(str);
- }
+ JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
index e1bbe0b604..5af1959cd6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
@@ -51,15 +51,16 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
- "error_message": {{JsonSerializer.Serialize(e.Message)}},
- "error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
- "inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
- "template_json": {{JsonSerializer.Serialize(TemplateJson)}},
- "data_json": {{JsonSerializer.Serialize(DataJson)}}
+ "error_message": {{serializeString(e.Message)}},
+ "error_stack": {{serializeString(e.StackTrace)}},
+ "inner_exception": {{serializeString(e.InnerException?.Message)}},
+ "template_json": {{serializeString(TemplateJson)}},
+ "data_json": {{serializeString(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
index 73bb041b99..f8b0e90834 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
@@ -167,7 +167,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
Commands.ForEach(contextItem =>
{
- contextItem.InitializeProperties();
+ contextItem.SlowInitializeProperties();
});
}
else
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs
new file mode 100644
index 0000000000..2b16bd8f47
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs
@@ -0,0 +1,82 @@
+// 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.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CmdPal.UI.ViewModels.Messages;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.System;
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public partial class ContextMenuStackViewModel : ObservableObject
+{
+ [ObservableProperty]
+ public partial ObservableCollection FilteredItems { get; set; }
+
+ private readonly IContextMenuContext _context;
+ private string _lastSearchText = string.Empty;
+
+ // private Dictionary? _contextKeybindings;
+ public ContextMenuStackViewModel(IContextMenuContext context)
+ {
+ _context = context;
+ FilteredItems = [.. context.AllCommands];
+ }
+
+ public void SetSearchText(string searchText)
+ {
+ if (searchText == _lastSearchText)
+ {
+ return;
+ }
+
+ _lastSearchText = searchText;
+
+ var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
+ if (string.IsNullOrEmpty(searchText))
+ {
+ ListHelpers.InPlaceUpdateList(FilteredItems, commands);
+ return;
+ }
+
+ var newResults = ListHelpers.FilterList(commands, searchText, ScoreContextCommand);
+ ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
+ }
+
+ private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
+ {
+ if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
+ {
+ return 1;
+ }
+
+ if (string.IsNullOrEmpty(item.Title))
+ {
+ return 0;
+ }
+
+ var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
+
+ var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
+
+ return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
+ }
+
+ public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ {
+ var keybindings = _context.Keybindings();
+ if (keybindings != null)
+ {
+ // Does the pressed key match any of the keybindings?
+ var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
+ if (keybindings.TryGetValue(pressedKeyChord, out var item))
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
index b45ea08f54..83dc4018f9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
@@ -344,8 +344,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send(new(item));
- WeakReferenceMessenger.Default.Send(new(item.Keybindings()));
-
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send(new(item.Details));
@@ -436,7 +434,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
break;
case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext);
- EmptyContent.InitializeProperties();
+ EmptyContent.SlowInitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
@@ -454,6 +452,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
return;
}
+ UpdateProperty(nameof(EmptyContent));
+
DoOnUiThread(
() =>
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
similarity index 59%
rename from src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs
rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
index 2054d3d8fd..3df48ec3a0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
@@ -2,8 +2,11 @@
// 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;
+using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
-public record UpdateItemKeybindingsMessage(Dictionary? Keys);
+public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
+{
+ public bool Handled { get; set; }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
index 0a540c7408..929b5995c5 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
+using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
-// Represents everything the command bar needs to know about to show command
-// buttons at the bottom.
-//
-// This is implemented by both ListItemViewModel and ContentPageViewModel,
-// the two things with sub-commands.
-public interface ICommandBarContext : INotifyPropertyChanged
+public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable MoreCommands { get; }
public bool HasMoreCommands { get; }
+ public List AllCommands { get; }
+
+ ///
+ /// Generates a mapping of key -> command item for this particular item's
+ /// MoreCommands. (This won't include the primary Command, but it will
+ /// include the secondary one). This map can be used to quickly check if a
+ /// shortcut key was pressed
+ ///
+ /// a dictionary of KeyChord -> Context commands, for all commands
+ /// that have a shortcut key set.
+ public Dictionary Keybindings()
+ {
+ return MoreCommands
+ .Where(c => c.HasRequestedShortcut)
+ .ToDictionary(
+ c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
+ c => c);
+ }
+}
+
+// Represents everything the command bar needs to know about to show command
+// buttons at the bottom.
+//
+// This is implemented by both ListItemViewModel and ContentPageViewModel,
+// the two things with sub-commands.
+public interface ICommandBarContext : IContextMenuContext
+{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
-
- public List AllCommands { get; }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
index 342dbe251c..8057bb09be 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
@@ -1,5 +1,7 @@
+
+
enable
enable
@@ -67,4 +69,15 @@
+
+
+
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 891c45ace9..ae97849f7a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -40,6 +40,8 @@ public partial class SettingsModel : ObservableObject
public bool ShowSystemTrayIcon { get; set; } = true;
+ public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
+
public Dictionary ProviderSettings { get; set; } = [];
public Dictionary Aliases { get; set; } = [];
@@ -91,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");
@@ -115,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)
@@ -131,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
@@ -164,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 b6c5214890..2083d35191 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -108,6 +108,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
+ 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/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index 38ea017125..203009f763 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -71,9 +71,9 @@
Padding="4"
ColumnSpacing="8">
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 e8ca659097..f079f4b513 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
@@ -18,9 +18,10 @@ 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
{
@@ -38,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)
@@ -52,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")]
@@ -88,8 +125,14 @@ 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();
+ }
}
}
@@ -106,9 +149,136 @@ public sealed partial class CommandBar : UserControl,
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
- if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
+ 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 d939381fb0..c868e3dd5e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
@@ -8,8 +8,6 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
-using Microsoft.CommandPalette.Extensions;
-using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient,
IRecipient,
- IRecipient,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
- private Dictionary? _keyBindings;
-
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
- WeakReferenceMessenger.Default.Register(this);
}
public void ClearSearch()
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send(new());
}
- if (_keyBindings != null)
+ if (!e.Handled)
{
- // Does the pressed key match any of the keybindings?
- var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
- if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
- {
- // TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
- // so that the correct item is activated.
- WeakReferenceMessenger.Default.Send(new(item));
- e.Handled = true;
- }
+ // 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;
}
}
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
- public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
-
- public void Receive(UpdateItemKeybindingsMessage message)
- {
- _keyBindings = message.Keys;
- }
+ public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
index 2a03983e1e..060b382f28 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml
@@ -138,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 be28b0edc3..ce20c9be77 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -43,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
@@ -157,6 +158,8 @@ public sealed partial class MainWindow : Window,
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.
AppWindow.IsShownInSwitchers = System.Diagnostics.Debugger.IsAttached;
@@ -504,6 +507,15 @@ 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];
HandleSummon(hotkey.CommandId);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index b45f685986..f97334466e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -356,9 +356,8 @@
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
index bc0999ca02..869b048dbd 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send(new(null));
- WeakReferenceMessenger.Default.Send(new(null));
-
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
index 68763ed5bb..497076bb0e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
@@ -48,6 +48,9 @@
+
+
+
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 9ffc14002b..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
@@ -328,6 +328,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
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
@@ -388,6 +394,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Behavior
+
+ Search commands...
+
Show system tray icon
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/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
index 9b3f54a21f..5da419cd40 100644
--- a/src/modules/cmdpal/ext/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/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/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
index 7c3a1dd1e0..8f2e257782 100644
--- a/src/modules/cmdpal/ext/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/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
index 7bbf1efc5f..40c3cca9f2 100644
--- a/src/modules/cmdpal/ext/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
@@ -16,7 +17,7 @@
-
+
Resources.resx
@@ -24,7 +25,7 @@
True
-
+
PreserveNewest
@@ -39,5 +40,5 @@
PublicResXFileCodeGenerator
-
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
index 85f06768ce..59f5ccbc70 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.Calc
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
index 50ff346103..f4b6089229 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
@@ -17,7 +17,7 @@ internal static class NativeMethods
internal INPUTTYPE type;
internal InputUnion data;
- internal static int Size => Marshal.SizeOf(typeof(INPUT));
+ internal static int Size => Marshal.SizeOf();
}
[StructLayout(LayoutKind.Explicit)]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
index 1d583e279b..774753f31d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.ClipboardHistory
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
index 2ff67f859f..c7633d7356 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
@@ -1,5 +1,7 @@
+
+
Microsoft.CmdPal.Ext.Registry
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
index c95f2a93b2..934e6d264a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
@@ -1,5 +1,7 @@
+
+
enable
Microsoft.CmdPal.Ext.Shell
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
index 9afb39b6f1..486eeaa8b5 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
@@ -311,14 +311,14 @@ internal sealed class NetworkConnectionProperties
{
switch (property)
{
- case string:
- return string.IsNullOrWhiteSpace(property) ? string.Empty : $"\n\n{title}{property}";
+ case string str:
+ return string.IsNullOrWhiteSpace(str) ? string.Empty : $"\n\n{title}{str}";
case List listString:
- return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listString)}";
case List listIP:
- return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listIP)}";
case IPAddressCollection collectionIP:
- return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", collectionIP)}";
case null:
return string.Empty;
default:
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
index 4c619bc5e5..48e9d6ba82 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
@@ -1,5 +1,6 @@
+
enable
Microsoft.CmdPal.Ext.System
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs
new file mode 100644
index 0000000000..267ab43989
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs
@@ -0,0 +1,104 @@
+// 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.Drawing;
+using System.Globalization;
+using System.Linq;
+using Microsoft.CmdPal.Ext.TimeDate.Helpers;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.TimeDate;
+
+internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
+{
+ private readonly HashSet _validOptions;
+ private SettingsManager _settingsManager;
+
+ public FallbackTimeDateItem(SettingsManager settings)
+ : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title)
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ _settingsManager = settings;
+ _validOptions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture),
+ };
+ }
+
+ public override void UpdateQuery(string query)
+ {
+ if (!_settingsManager.EnableFallbackItems || string.IsNullOrWhiteSpace(query) || !IsValidQuery(query))
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new NoOpCommand();
+ return;
+ }
+
+ var availableResults = AvailableResultsList.GetList(false, _settingsManager);
+ ListItem result = null;
+ var maxScore = 0;
+
+ foreach (var f in availableResults)
+ {
+ var score = f.Score(query, f.Label, f.AlternativeSearchTag);
+ if (score > maxScore)
+ {
+ maxScore = score;
+ result = f.ToListItem();
+ }
+ }
+
+ if (result != null)
+ {
+ Title = result.Title;
+ Subtitle = result.Subtitle;
+ Icon = result.Icon;
+ }
+ else
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new NoOpCommand();
+ }
+ }
+
+ private bool IsValidQuery(string query)
+ {
+ if (_validOptions.Contains(query))
+ {
+ return true;
+ }
+
+ foreach (var option in _validOptions)
+ {
+ if (option == null)
+ {
+ continue;
+ }
+
+ var parts = option.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ if (parts.Any(part => string.Equals(part, query, StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
index bc6a3b972c..60ccaf38b5 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
@@ -33,6 +33,7 @@ internal static class AvailableResultsList
var dateTimeNowUtc = dateTimeNow.ToUniversalTime();
var firstWeekRule = firstWeekOfYear ?? TimeAndDateHelper.GetCalendarWeekRule(settings.FirstWeekOfYear);
var firstDayOfTheWeek = firstDayOfWeek ?? TimeAndDateHelper.GetFirstDayOfWeek(settings.FirstDayOfWeek);
+ var weekOfYear = calendar.GetWeekOfYear(dateTimeNow, firstWeekRule, firstDayOfTheWeek);
results.AddRange(new[]
{
@@ -59,14 +60,20 @@ internal static class AvailableResultsList
AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"),
IconType = ResultIconType.DateTime,
},
+ new AvailableResult()
+ {
+ Value = weekOfYear.ToString(CultureInfo.CurrentCulture),
+ Label = Resources.Microsoft_plugin_timedate_WeekOfYear,
+ AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"),
+ IconType = ResultIconType.Date,
+ },
});
- if (isKeywordSearch || !settings.OnlyDateTimeNowGlobal)
+ if (isKeywordSearch)
{
// We use long instead of int for unix time stamp because int is too small after 03:14:07 UTC 2038-01-19
var unixTimestamp = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeSeconds();
var unixTimestampMilliseconds = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeMilliseconds();
- var weekOfYear = calendar.GetWeekOfYear(dateTimeNow, firstWeekRule, firstDayOfTheWeek);
var era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow));
var eraShort = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(calendar.GetEra(dateTimeNow));
@@ -251,13 +258,6 @@ internal static class AvailableResultsList
IconType = ResultIconType.Date,
},
new AvailableResult()
- {
- Value = weekOfYear.ToString(CultureInfo.CurrentCulture),
- Label = Resources.Microsoft_plugin_timedate_WeekOfYear,
- AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"),
- IconType = ResultIconType.Date,
- },
- new AvailableResult()
{
Value = DateTimeFormatInfo.CurrentInfo.GetMonthName(dateTimeNow.Month),
Label = Resources.Microsoft_plugin_timedate_Month,
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
index 5b30a4816f..7b351fe3b8 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
@@ -75,11 +75,11 @@ public class SettingsManager : JsonSettingsManager
Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek,
_firstDayOfWeekChoices);
- private readonly ToggleSetting _onlyDateTimeNowGlobal = new(
- Namespaced(nameof(OnlyDateTimeNowGlobal)),
- Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal,
- Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description,
- true); // TODO -- double check default value
+ private readonly ToggleSetting _enableFallbackItems = new(
+ Namespaced(nameof(EnableFallbackItems)),
+ Resources.Microsoft_plugin_timedate_SettingEnableFallbackItems,
+ Resources.Microsoft_plugin_timedate_SettingEnableFallbackItems_Description,
+ true);
private readonly ToggleSetting _timeWithSeconds = new(
Namespaced(nameof(TimeWithSecond)),
@@ -93,12 +93,6 @@ public class SettingsManager : JsonSettingsManager
Resources.Microsoft_plugin_timedate_SettingDateWithWeekday_Description,
false); // TODO -- double check default value
- private readonly ToggleSetting _hideNumberMessageOnGlobalQuery = new(
- Namespaced(nameof(HideNumberMessageOnGlobalQuery)),
- Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery,
- Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery,
- true); // TODO -- double check default value
-
private readonly TextSetting _customFormats = new(
Namespaced(nameof(CustomFormats)),
Resources.Microsoft_plugin_timedate_Setting_CustomFormats,
@@ -145,14 +139,12 @@ public class SettingsManager : JsonSettingsManager
}
}
- public bool OnlyDateTimeNowGlobal => _onlyDateTimeNowGlobal.Value;
+ public bool EnableFallbackItems => _enableFallbackItems.Value;
public bool TimeWithSecond => _timeWithSeconds.Value;
public bool DateWithWeekday => _dateWithWeekday.Value;
- public bool HideNumberMessageOnGlobalQuery => _hideNumberMessageOnGlobalQuery.Value;
-
public List CustomFormats => _customFormats.Value.Split(TEXTBOXNEWLINE).ToList();
internal static string SettingsJsonPath()
@@ -168,10 +160,7 @@ public class SettingsManager : JsonSettingsManager
{
FilePath = SettingsJsonPath();
- /* The following two settings make no sense with current CmdPal behavior.
- Settings.Add(_onlyDateTimeNowGlobal);
- Settings.Add(_hideNumberMessageOnGlobalQuery); */
-
+ Settings.Add(_enableFallbackItems);
Settings.Add(_timeWithSeconds);
Settings.Add(_dateWithWeekday);
Settings.Add(_firstWeekOfYear);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
index eb4eed18cd..38f417ad5b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
@@ -40,7 +40,7 @@ public sealed partial class TimeDateCalculator
var lastInputParsingErrorMsg = string.Empty;
// Switch search type
- if (isEmptySearchInput || (!isKeywordSearch && settings.OnlyDateTimeNowGlobal))
+ if (isEmptySearchInput || (!isKeywordSearch))
{
// Return all results for system time/date on empty keyword search
// or only time, date and now results for system time on global queries if the corresponding setting is enabled
@@ -91,23 +91,6 @@ public sealed partial class TimeDateCalculator
}
}
- /*htcfreek:Code obsolete with current CmdPal behavior.
- // If search term is only a number that can't be parsed return an error message
- if (!isEmptySearchInput && results.Count == 0 && Regex.IsMatch(query, @"\w+\d+.*$") && !query.Any(char.IsWhiteSpace) && (TimeAndDateHelper.IsSpecialInputParsing(query) || !Regex.IsMatch(query, @"\d+[\.:/]\d+")))
- {
- // Without plugin key word show only if message is not hidden by setting
- if (!settings.HideNumberMessageOnGlobalQuery)
- {
- var er = ResultHelper.CreateInvalidInputErrorResult();
- if (!string.IsNullOrEmpty(lastInputParsingErrorMsg))
- {
- er.Details = new Details() { Body = lastInputParsingErrorMsg };
- }
-
- results.Add(er);
- }
- } */
-
if (results.Count == 0)
{
var er = ResultHelper.CreateInvalidInputErrorResult();
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
index 733ed8634e..34301712cf 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
@@ -1,6 +1,7 @@
-
+
+
Microsoft.CmdPal.Ext.TimeDate
false
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
index d62d76eefd..a21759d10a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
@@ -213,6 +213,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Open Time Data Command.
+ ///
+ public static string Microsoft_plugin_timedate_fallback_display_title {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_fallback_display_title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Date and time in filename-compatible format.
///
@@ -609,6 +618,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Current Week; Calendar week; Week of the year; Week.
+ ///
+ public static string Microsoft_plugin_timedate_SearchTagWeek {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Second.
///
@@ -663,6 +681,24 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Enable fallback items for TimeDate (week, year, now, time, date).
+ ///
+ public static string Microsoft_plugin_timedate_SettingEnableFallbackItems {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SettingEnableFallbackItems", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show time and date results when typing keywords like "week", "year", "now", "time", or "date".
+ ///
+ public static string Microsoft_plugin_timedate_SettingEnableFallbackItems_Description {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SettingEnableFallbackItems_Description", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to First day of the week.
///
@@ -780,33 +816,6 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
- ///
- /// Looks up a localized string similar to Hide 'Invalid number input' error message on global queries.
- ///
- public static string Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Show only 'Time', 'Date' and 'Now' result for system time on global queries.
- ///
- public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Regardless of this setting, for global queries the first word of the query has to be a complete match..
- ///
- public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Show time with seconds.
///
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
index f1a36e2a90..35862592ca 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
@@ -265,15 +265,6 @@
This setting applies to the 'Date' and 'Now' result.
-
- Hide 'Invalid number input' error message on global queries
-
-
- Show only 'Time', 'Date' and 'Now' result for system time on global queries
-
-
- Regardless of this setting, for global queries the first word of the query has to be a complete match.
-
Show time with seconds
@@ -433,4 +424,16 @@
Days in month
+
+ Open Time Data Command
+
+
+ Current Week; Calendar week; Week of the year; Week
+
+
+ Enable fallback items for TimeDate (week, year, now, time, date)
+
+
+ Show time and date results when typing keywords like "week", "year", "now", "time", or "date"
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
index 05597c4553..d29356fa77 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
@@ -18,6 +18,7 @@ public partial class TimeDateCommandsProvider : CommandProvider
private static readonly SettingsManager _settingsManager = new();
private static readonly CompositeFormat MicrosoftPluginTimedatePluginDescription = System.Text.CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_plugin_description);
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
+ private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
public TimeDateCommandsProvider()
{
@@ -45,4 +46,6 @@ public partial class TimeDateCommandsProvider : CommandProvider
}
public override ICommandItem[] TopLevelCommands() => [_command];
+
+ public override IFallbackCommandItem[] FallbackCommands() => [_fallbackTimeDateItem];
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
index 84a1c249ba..d381c1e4cc 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
@@ -13,5 +13,5 @@ public class HistoryItem(string searchString, DateTime timestamp)
public DateTime Timestamp { get; private set; } = timestamp;
- public string ToJson() => JsonSerializer.Serialize(this);
+ public string ToJson() => JsonSerializer.Serialize(this, WebSearchJsonSerializationContext.Default.HistoryItem);
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
index b83ba47a73..8a39bca35b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
@@ -80,7 +80,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
- historyItems = JsonSerializer.Deserialize>(existingContent) ?? [];
+ historyItems = JsonSerializer.Deserialize>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
@@ -101,7 +101,7 @@ public class SettingsManager : JsonSettingsManager
}
// Serialize the updated list back to JSON and save it
- var historyJson = JsonSerializer.Serialize(historyItems);
+ var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
}
catch (Exception ex)
@@ -121,7 +121,7 @@ public class SettingsManager : JsonSettingsManager
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
- var historyItems = JsonSerializer.Deserialize>(fileContent) ?? [];
+ var historyItems = JsonSerializer.Deserialize>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List();
@@ -198,7 +198,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
- var historyItems = JsonSerializer.Deserialize>(existingContent) ?? [];
+ var historyItems = JsonSerializer.Deserialize>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > maxHistoryItems)
@@ -207,7 +207,7 @@ public class SettingsManager : JsonSettingsManager
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
- var trimmedHistoryJson = JsonSerializer.Serialize(historyItems);
+ var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs
new file mode 100644
index 0000000000..443c9cdf40
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.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;
+using Microsoft.CmdPal.Ext.WebSearch.Helpers;
+
+namespace Microsoft.CmdPal.Ext.WebSearch;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(HistoryItem))]
+[JsonSerializable(typeof(List))]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class WebSearchJsonSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
index 3ddedfcd71..3fbaeb30a7 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.WebSearch
enable
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
index 1aa22bfa26..cf85359bdf 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
@@ -22,10 +22,12 @@ public partial class InstallPackageCommand : InvokableCommand
private IAsyncOperationWithProgress? _unInstallAction;
private Task? _installTask;
- public bool IsInstalled { get; private set; }
+ public PackageInstallCommandState InstallCommandState { get; private set; }
public static IconInfo CompletedIcon { get; } = new("\uE930"); // Completed
+ public static IconInfo UpdateIcon { get; } = new("\uE74A"); // Up
+
public static IconInfo DownloadIcon { get; } = new("\uE896"); // Download
public static IconInfo DeleteIcon { get; } = new("\uE74D"); // Delete
@@ -44,23 +46,41 @@ public partial class InstallPackageCommand : InvokableCommand
internal bool SkipDependencies { get; set; }
- public InstallPackageCommand(CatalogPackage package, bool isInstalled)
+ public InstallPackageCommand(CatalogPackage package, PackageInstallCommandState isInstalled)
{
_package = package;
- IsInstalled = isInstalled;
+ InstallCommandState = isInstalled;
UpdateAppearance();
}
internal void FakeChangeStatus()
{
- IsInstalled = !IsInstalled;
+ InstallCommandState = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => PackageInstallCommandState.Uninstall,
+ PackageInstallCommandState.Update => PackageInstallCommandState.Uninstall,
+ PackageInstallCommandState.Uninstall => PackageInstallCommandState.Install,
+ _ => throw new NotImplementedException(),
+ };
UpdateAppearance();
}
private void UpdateAppearance()
{
- Icon = IsInstalled ? CompletedIcon : DownloadIcon;
- Name = IsInstalled ? Properties.Resources.winget_uninstall_name : Properties.Resources.winget_install_name;
+ Icon = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => DownloadIcon,
+ PackageInstallCommandState.Update => UpdateIcon,
+ PackageInstallCommandState.Uninstall => CompletedIcon,
+ _ => throw new NotImplementedException(),
+ };
+ Name = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => Properties.Resources.winget_install_name,
+ PackageInstallCommandState.Update => Properties.Resources.winget_update_name,
+ PackageInstallCommandState.Uninstall => Properties.Resources.winget_uninstall_name,
+ _ => throw new NotImplementedException(),
+ };
}
public override ICommandResult Invoke()
@@ -72,7 +92,7 @@ public partial class InstallPackageCommand : InvokableCommand
return CommandResult.KeepOpen();
}
- if (IsInstalled)
+ if (InstallCommandState == PackageInstallCommandState.Uninstall)
{
// Uninstall
_installBanner.State = MessageState.Info;
@@ -88,7 +108,8 @@ public partial class InstallPackageCommand : InvokableCommand
_installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction));
}
- else
+ else if (InstallCommandState is PackageInstallCommandState.Install or
+ PackageInstallCommandState.Update)
{
// Install
_installBanner.State = MessageState.Info;
@@ -117,7 +138,8 @@ public partial class InstallPackageCommand : InvokableCommand
try
{
await action.AsTask();
- _installBanner.Message = IsInstalled ?
+
+ _installBanner.Message = InstallCommandState == PackageInstallCommandState.Uninstall ?
string.Format(CultureInfo.CurrentCulture, UninstallPackageFinished, _package.Name) :
string.Format(CultureInfo.CurrentCulture, InstallPackageFinished, _package.Name);
@@ -125,9 +147,10 @@ public partial class InstallPackageCommand : InvokableCommand
_installBanner.State = MessageState.Success;
_installTask = null;
- _ = Task.Run(() =>
+ _ = Task.Run(async () =>
{
- Thread.Sleep(2500);
+ await Task.Delay(2500).ConfigureAwait(false);
+
if (_installTask == null)
{
WinGetExtensionHost.Instance.HideStatus(_installBanner);
@@ -228,3 +251,10 @@ public partial class InstallPackageCommand : InvokableCommand
}
}
}
+
+public enum PackageInstallCommandState
+{
+ Uninstall = 0,
+ Update = 1,
+ Install = 2,
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
index 2ce192f262..5c556ec3cb 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
@@ -31,7 +32,7 @@ public partial class InstallPackageListItem : ListItem
{
_package = package;
- var version = _package.DefaultInstallVersion;
+ var version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
var versionTagText = "Unknown";
if (version != null)
{
@@ -49,7 +50,16 @@ public partial class InstallPackageListItem : ListItem
private Details? BuildDetails(PackageVersionInfo? version)
{
- var metadata = version?.GetCatalogPackageMetadata();
+ CatalogPackageMetadata? metadata = null;
+ try
+ {
+ metadata = version?.GetCatalogPackageMetadata();
+ }
+ catch (COMException ex)
+ {
+ Logger.LogWarning($"{ex.ErrorCode}");
+ }
+
if (metadata != null)
{
if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any())
@@ -149,12 +159,17 @@ public partial class InstallPackageListItem : ListItem
var status = await _package.CheckInstalledStatusAsync();
var isInstalled = _package.InstalledVersion != null;
+ var installedState = isInstalled ?
+ (_package.IsUpdateAvailable ?
+ PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
+ PackageInstallCommandState.Install;
+
// might be an uninstall command
- InstallPackageCommand installCommand = new(_package, isInstalled);
+ InstallPackageCommand installCommand = new(_package, installedState);
if (isInstalled)
{
- this.Icon = InstallPackageCommand.CompletedIcon;
+ this.Icon = installCommand.Icon;
this.Command = new NoOpCommand();
List contextMenu = [];
CommandContextItem uninstallContextItem = new(installCommand)
@@ -180,7 +195,7 @@ public partial class InstallPackageListItem : ListItem
}
// didn't find the app
- _installCommand = new InstallPackageCommand(_package, isInstalled);
+ _installCommand = new InstallPackageCommand(_package, installedState);
this.Command = _installCommand;
Icon = _installCommand.Icon;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
index f62a291d36..418cccf58c 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
@@ -330,6 +330,15 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Update.
+ ///
+ public static string winget_update_name {
+ get {
+ return ResourceManager.GetString("winget_update_name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to View online.
///
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
index 2be07cec68..ac2128430d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
@@ -154,6 +154,10 @@
Install
+
+ Update
+
+
Uninstalling {0}...
{0} will be replaced by the name of an app package
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
index 2ed59ad6a3..e346e824c8 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
@@ -1,5 +1,7 @@
+
+
enable
Microsoft.CmdPal.Ext.WindowWalker
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
index 2b7b9345ec..9038a2d671 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
@@ -1,5 +1,7 @@
+
+
Microsoft.CmdPal.Ext.WindowsServices
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
index f99f0ce3b3..18b2548ce2 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
@@ -4,10 +4,8 @@
using System;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers;
@@ -21,6 +19,8 @@ internal static class JsonSettingsListHelper
///
private const string _settingsFile = "WindowsSettings.json";
+ private const string _extTypeNamespace = "Microsoft.CmdPal.Ext.WindowsSettings";
+
private static readonly JsonSerializerOptions _serializerOptions = new()
{
};
@@ -32,7 +32,6 @@ internal static class JsonSettingsListHelper
internal static Classes.WindowsSettings ReadAllPossibleSettings()
{
var assembly = Assembly.GetExecutingAssembly();
- var type = assembly.GetTypes().FirstOrDefault(x => x.Name == nameof(WindowsSettingsCommandsProvider));
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Classes.WindowsSettings? settings = null;
@@ -40,7 +39,7 @@ internal static class JsonSettingsListHelper
try
{
- var resourceName = $"{type?.Namespace}.{_settingsFile}";
+ var resourceName = $"{_extTypeNamespace}.{_settingsFile}";
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
@@ -48,12 +47,13 @@ internal static class JsonSettingsListHelper
}
var options = _serializerOptions;
- options.Converters.Add(new JsonStringEnumConverter());
+ // Why we need it? I don't see any enum usage in WindowsSettings
+ // options.Converters.Add(new JsonStringEnumConverter());
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
- settings = JsonSerializer.Deserialize(text, options);
+ settings = JsonSerializer.Deserialize(text, WindowsSettingsJsonSerializationContext.Default.WindowsSettings);
}
#pragma warning disable CS0168
catch (Exception exception)
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs
new file mode 100644
index 0000000000..e267e8f52e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs
@@ -0,0 +1,22 @@
+// 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;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Microsoft.CmdPal.Ext.WindowsSettings;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(Classes.WindowsSettings))]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class WindowsSettingsJsonSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
index d44e907606..9f2d72bc8a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.WindowsSettings
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
index 7ea6f17148..1c09c35c5d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
@@ -1,6 +1,8 @@
+
+
Microsoft.CmdPal.Ext.WindowsTerminal
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
index 373a1f7891..2fc1218bd7 100644
--- a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.System;
namespace SamplePagesExtension;
@@ -76,7 +77,136 @@ public partial class EvilSamplesPage : ListPage
{
Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
},
- }
+ },
+
+ // More edge cases than truly evil
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "anonymous command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
+ {
+ Title = "I'm a second command",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
+ },
+ new CommandContextItem("nested...")
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "noop command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
+ {
+ Title = "I'm a second command",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
+ },
+ new CommandContextItem(new NoOpCommand())
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "noop secondary command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(new NoOpCommand())
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+
];
public EvilSamplesPage()
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
index 954a79ce04..3cf987e417 100644
--- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
@@ -69,62 +69,47 @@ internal sealed partial class SampleListPage : ListPage
},
new ListItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Primary command invoked",
- State = MessageState.Info,
- });
- t.Show();
- })
- {
- Result = CommandResult.KeepOpen(),
- Icon = new IconInfo("\uE712"),
- })
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "You can add context menu items too. Press Ctrl+k",
Subtitle = "Try pressing Ctrl+1 with me selected",
- Icon = new IconInfo("\uE712"),
+ Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Secondary command invoked",
- State = MessageState.Warning,
- });
- t.Show();
- })
- {
- Name = "Secondary command",
- Icon = new IconInfo("\uF147"), // Dial 2
- Result = CommandResult.KeepOpen(),
- })
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Third command invoked",
- State = MessageState.Error,
- });
- t.Show();
- })
- {
- Name = "Do it",
- Icon = new IconInfo("\uF148"), // dial 3
- Result = CommandResult.KeepOpen(),
- })
+ new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
{
- Title = "A third command too",
+ Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
}
],
},
@@ -183,7 +168,6 @@ internal sealed partial class SampleListPage : ListPage
{
Title = "Get the name of the Foreground window",
},
-
];
}
}
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs
new file mode 100644
index 0000000000..dfbeb5225a
--- /dev/null
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs
@@ -0,0 +1,23 @@
+// 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;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace SamplePagesExtension;
+
+internal sealed partial class ToastCommand(string message, MessageState state = MessageState.Info) : InvokableCommand
+{
+ public override ICommandResult Invoke()
+ {
+ var t = new ToastStatusMessage(new StatusMessage()
+ {
+ Message = message,
+ State = state,
+ });
+ t.Show();
+
+ return CommandResult.KeepOpen();
+ }
+}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
index 6fbc734560..6d92cdc146 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
@@ -16,7 +16,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
[JsonSerializable(typeof(List))]
[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
-[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal partial class JsonSerializationContext : JsonSerializerContext
{
}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
index 91d715b509..ffd20643aa 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
@@ -61,4 +61,9 @@ public partial class ListItem : CommandItem, IListItem
: base(command)
{
}
+
+ public ListItem()
+ : base()
+ {
+ }
}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
index 1090e58f25..0f87df5625 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
@@ -2,7 +2,7 @@
..\..\..\..\..\
- $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.6.250205002
+ $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001
$(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5
$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428
$(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
index 93d095a2b1..608661db25 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
index 82b238993d..3f0feaaae3 100644
--- a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
+++ b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
@@ -243,6 +243,40 @@ namespace ColorPicker.Helpers
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
+ ///
+ /// Returns a representation of a Oklab color
+ ///
+ /// The for the Oklab color presentation
+ /// A representation of a Oklab color
+ private static string ColorToOklab(Color color)
+ {
+ var (lightness, chromaticityA, chromaticityB) = ColorFormatHelper.ConvertToOklabColor(color);
+ lightness = Math.Round(lightness, 2);
+ chromaticityA = Math.Round(chromaticityA, 2);
+ chromaticityB = Math.Round(chromaticityB, 2);
+
+ return $"oklab({lightness.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
+ }
+
+ ///
+ /// Returns a representation of a CIE LCh color
+ ///
+ /// The for the CIE LCh color presentation
+ /// A representation of a CIE LCh color
+ private static string ColorToOklch(Color color)
+ {
+ var (lightness, chroma, hue) = ColorFormatHelper.ConvertToOklchColor(color);
+ lightness = Math.Round(lightness, 2);
+ chroma = Math.Round(chroma, 2);
+ hue = Math.Round(hue, 2);
+
+ return $"oklch({lightness.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chroma.ToString(CultureInfo.InvariantCulture)}" +
+ $", {hue.ToString(CultureInfo.InvariantCulture)})";
+ }
+
///
/// Returns a representation of a CIE XYZ color
///
diff --git a/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs b/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
index 129f365e0d..2f8d5a2348 100644
--- a/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
+++ b/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
@@ -301,6 +301,12 @@ namespace ColorPicker.ViewModels
FormatName = ColorRepresentationType.NCol.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol.ToString()),
});
+ _allColorRepresentations.Add(
+ new ColorFormatModel()
+ {
+ FormatName = ColorRepresentationType.CIEXYZ.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
+ });
_allColorRepresentations.Add(
new ColorFormatModel()
{
@@ -310,8 +316,14 @@ namespace ColorPicker.ViewModels
_allColorRepresentations.Add(
new ColorFormatModel()
{
- FormatName = ColorRepresentationType.CIEXYZ.ToString(),
- Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
+ FormatName = ColorRepresentationType.Oklab.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklab.ToString()),
+ });
+ _allColorRepresentations.Add(
+ new ColorFormatModel()
+ {
+ FormatName = ColorRepresentationType.Oklch.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklch.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()
diff --git a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
index eaa5369dd6..288ed0f599 100644
--- a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
+++ b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
@@ -364,9 +364,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
[DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("BFBF00", 75.04, -17.35, 76.03)] // yellow
- [DataRow("008000", 46.23, -51.70, 49.90)] // green
- [DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
- [DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("0048BA", 34.35, 27.94, -64.80)] // absolute zero
[DataRow("B0BF1A", 73.91, -23.39, 71.15)] // acid green
[DataRow("D0FF14", 93.87, -40.20, 88.97)] // arctic lime
@@ -401,13 +398,121 @@ namespace Microsoft.ColorPicker.UnitTests
var result = ColorFormatHelper.ConvertToCIELABColor(color);
// lightness[0..100]
- Assert.AreEqual(Math.Round(result.Lightness, 2), lightness);
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 2));
// chromaticityA[-128..127]
- Assert.AreEqual(Math.Round(result.ChromaticityA, 2), chromaticityA);
+ Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
// chromaticityB[-128..127]
- Assert.AreEqual(Math.Round(result.ChromaticityB, 2), chromaticityB);
+ Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
+ }
+
+ // Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
+ [TestMethod]
+ [DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
+ [DataRow("808080", 0.6, 0.00, 0.00)] // gray
+ [DataRow("000000", 0.00, 0.00, 0.00)] // black
+ [DataRow("FF0000", 0.628, 0.22, 0.13)] // red
+ [DataRow("008000", 0.52, -0.14, 0.11)] // green
+ [DataRow("80FFFF", 0.928, -0.11, -0.03)] // cyan
+ [DataRow("8080FF", 0.661, 0.03, -0.18)] // blue
+ [DataRow("BF40BF", 0.598, 0.18, -0.11)] // magenta
+ [DataRow("BFBF00", 0.779, -0.06, 0.16)] // yellow
+ [DataRow("0048BA", 0.444, -0.03, -0.19)] // absolute zero
+ [DataRow("B0BF1A", 0.767, -0.07, 0.15)] // acid green
+ [DataRow("D0FF14", 0.934, -0.12, 0.19)] // arctic lime
+ [DataRow("1B4D3E", 0.382, -0.06, 0.01)] // brunswick green
+ [DataRow("FFEF00", 0.935, -0.05, 0.19)] // canary yellow
+ [DataRow("FFA600", 0.794, 0.06, 0.16)] // cheese
+ [DataRow("1A2421", 0.25, -0.02, 0)] // dark jungle green
+ [DataRow("003399", 0.371, -0.02, -0.17)] // dark powder blue
+ [DataRow("D70A53", 0.563, 0.22, 0.04)] // debian red
+ [DataRow("80FFD5", 0.916, -0.13, 0.02)] // fathom secret green
+ [DataRow("EFDFBB", 0.907, 0, 0.05)] // dutch white
+ [DataRow("5218FA", 0.489, 0.05, -0.28)] // han purple
+ [DataRow("FF496C", 0.675, 0.21, 0.05)] // infra red
+ [DataRow("545AA7", 0.5, 0.02, -0.12)] // liberty
+ [DataRow("E6A8D7", 0.804, 0.09, -0.04)] // light orchid
+ [DataRow("ADDFAD", 0.856, -0.07, 0.05)] // light moss green
+ [DataRow("E3F988", 0.942, -0.07, 0.12)] // mindaro
+ public void ColorRGBtoOklabTest(string hexValue, double lightness, double chromaticityA, double chromaticityB)
+ {
+ if (string.IsNullOrWhiteSpace(hexValue))
+ {
+ Assert.IsNotNull(hexValue);
+ }
+
+ Assert.IsTrue(hexValue.Length >= 6);
+
+ var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+
+ var color = Color.FromArgb(255, red, green, blue);
+ var result = ColorFormatHelper.ConvertToOklabColor(color);
+
+ // lightness[0..1]
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
+
+ // chromaticityA[-0.5..0.5]
+ Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
+
+ // chromaticityB[-0.5..0.5]
+ Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
+ }
+
+ // Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
+ [TestMethod]
+ [DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
+ [DataRow("808080", 0.6, 0.00, 0.00)] // gray
+ [DataRow("000000", 0.00, 0.00, 0.00)] // black
+ [DataRow("FF0000", 0.628, 0.258, 29.23)] // red
+ [DataRow("008000", 0.52, 0.177, 142.5)] // green
+ [DataRow("80FFFF", 0.928, 0.113, 195.38)] // cyan
+ [DataRow("8080FF", 0.661, 0.184, 280.13)] // blue
+ [DataRow("BF40BF", 0.598, 0.216, 327.86)] // magenta
+ [DataRow("BFBF00", 0.779, 0.17, 109.77)] // yellow
+ [DataRow("0048BA", 0.444, 0.19, 260.86)] // absolute zero
+ [DataRow("B0BF1A", 0.767, 0.169, 115.4)] // acid green
+ [DataRow("D0FF14", 0.934, 0.224, 122.28)] // arctic lime
+ [DataRow("1B4D3E", 0.382, 0.06, 170.28)] // brunswick green
+ [DataRow("FFEF00", 0.935, 0.198, 104.67)] // canary yellow
+ [DataRow("FFA600", 0.794, 0.171, 71.19)] // cheese
+ [DataRow("1A2421", 0.25, 0.015, 174.74)] // dark jungle green
+ [DataRow("003399", 0.371, 0.173, 262.12)] // dark powder blue
+ [DataRow("D70A53", 0.563, 0.222, 11.5)] // debian red
+ [DataRow("80FFD5", 0.916, 0.129, 169.38)] // fathom secret green
+ [DataRow("EFDFBB", 0.907, 0.05, 86.89)] // dutch white
+ [DataRow("5218FA", 0.489, 0.286, 279.13)] // han purple
+ [DataRow("FF496C", 0.675, 0.217, 14.37)] // infra red
+ [DataRow("545AA7", 0.5, 0.121, 277.7)] // liberty
+ [DataRow("E6A8D7", 0.804, 0.095, 335.4)] // light orchid
+ [DataRow("ADDFAD", 0.856, 0.086, 144.78)] // light moss green
+ [DataRow("E3F988", 0.942, 0.141, 118.24)] // mindaro
+ public void ColorRGBtoOklchTest(string hexValue, double lightness, double chroma, double hue)
+ {
+ if (string.IsNullOrWhiteSpace(hexValue))
+ {
+ Assert.IsNotNull(hexValue);
+ }
+
+ Assert.IsTrue(hexValue.Length >= 6);
+
+ var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+
+ var color = Color.FromArgb(255, red, green, blue);
+ var result = ColorFormatHelper.ConvertToOklchColor(color);
+
+ // lightness[0..1]
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
+
+ // chroma[0..0.5]
+ Assert.AreEqual(chroma, Math.Round(result.Chroma, 3));
+
+ // hue[0°..360°]
+ Assert.AreEqual(hue, Math.Round(result.Hue, 2));
}
// The following results are computed using LittleCMS2, an open-source color management engine,
@@ -428,9 +533,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
[DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("BFBF00", 40.1154, 48.3384, 7.2171)] // yellow
- [DataRow("008000", 7.7188, 15.4377, 2.5729)] // green
- [DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
- [DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("0048BA", 11.1792, 8.1793, 47.4455)] // absolute zero
[DataRow("B0BF1A", 36.7205, 46.5663, 8.0311)] // acid green
[DataRow("D0FF14", 61.8965, 84.9797, 13.8037)] // arctic lime
diff --git a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
index a96310dc82..f1f0c99e3d 100644
--- a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
+++ b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
@@ -23,8 +23,10 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("HSV", "hsv(0, 0%, 0%)")]
[DataRow("HWB", "hwb(0, 0%, 100%)")]
[DataRow("RGB", "rgb(0, 0, 0)")]
- [DataRow("CIELAB", "CIELab(0, 0, 0)")]
[DataRow("CIEXYZ", "XYZ(0, 0, 0)")]
+ [DataRow("CIELAB", "CIELab(0, 0, 0)")]
+ [DataRow("Oklab", "oklab(0, 0, 0)")]
+ [DataRow("Oklch", "oklch(0, 0, 0)")]
[DataRow("VEC4", "(0f, 0f, 0f, 1f)")]
[DataRow("Decimal", "0")]
[DataRow("HEX Int", "0xFF000000")]
diff --git a/src/modules/imageresizer/ui/Models/ResizeSize.cs b/src/modules/imageresizer/ui/Models/ResizeSize.cs
index ebfca83963..49869f4bd2 100644
--- a/src/modules/imageresizer/ui/Models/ResizeSize.cs
+++ b/src/modules/imageresizer/ui/Models/ResizeSize.cs
@@ -11,10 +11,11 @@ using System.Text.Json.Serialization;
using ImageResizer.Helpers;
using ImageResizer.Properties;
+using ManagedCommon;
namespace ImageResizer.Models
{
- public class ResizeSize : Observable
+ public class ResizeSize : Observable, IHasId
{
private static readonly Dictionary _tokens = new Dictionary
{
@@ -24,6 +25,7 @@ namespace ImageResizer.Models
["$phone$"] = Resources.Phone,
};
+ private int _id;
private string _name;
private ResizeFit _fit = ResizeFit.Fit;
private double _width;
@@ -31,8 +33,9 @@ namespace ImageResizer.Models
private bool _showHeight = true;
private ResizeUnit _unit = ResizeUnit.Pixel;
- public ResizeSize(string name, ResizeFit fit, double width, double height, ResizeUnit unit)
+ public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
{
+ Id = id;
Name = name;
Fit = fit;
Width = width;
@@ -44,6 +47,13 @@ namespace ImageResizer.Models
{
}
+ [JsonPropertyName("Id")]
+ public int Id
+ {
+ get => _id;
+ set => Set(ref _id, value);
+ }
+
[JsonPropertyName("name")]
public virtual string Name
{
diff --git a/src/modules/imageresizer/ui/Properties/Settings.cs b/src/modules/imageresizer/ui/Properties/Settings.cs
index 4d7ebb3848..debb26a191 100644
--- a/src/modules/imageresizer/ui/Properties/Settings.cs
+++ b/src/modules/imageresizer/ui/Properties/Settings.cs
@@ -19,6 +19,7 @@ using System.Threading;
using System.Windows.Media.Imaging;
using ImageResizer.Models;
+using ManagedCommon;
namespace ImageResizer.Properties
{
@@ -63,10 +64,10 @@ namespace ImageResizer.Properties
FileName = "%1 (%2)";
Sizes = new ObservableCollection
{
- new ResizeSize("$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
- new ResizeSize("$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
- new ResizeSize("$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
- new ResizeSize("$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
+ new ResizeSize(0, "$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
+ new ResizeSize(1, "$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
+ new ResizeSize(2, "$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
+ new ResizeSize(3, "$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
};
KeepDateModified = false;
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
@@ -480,6 +481,9 @@ namespace ImageResizer.Properties
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
+
+ // Ensure Ids are unique and handle missing Ids
+ IdRecoveryHelper.RecoverInvalidIds(Sizes);
}
});
diff --git a/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
index 02e6138b30..64a52e8385 100644
--- a/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
+++ b/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
@@ -282,11 +282,11 @@ namespace PowerLauncher.ViewModel
if (options.SearchQueryTuningEnabled)
{
- sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * options.SearchClickedItemWeight))).ToList();
+ sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(options.SearchClickedItemWeight)).ToList();
}
else
{
- sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * 5))).ToList();
+ sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(5)).ToList();
}
// remove history items in they are in the list as non-history items
diff --git a/src/modules/launcher/Wox.Plugin/Result.cs b/src/modules/launcher/Wox.Plugin/Result.cs
index 91f026bbb2..3bbb6dbf5e 100644
--- a/src/modules/launcher/Wox.Plugin/Result.cs
+++ b/src/modules/launcher/Wox.Plugin/Result.cs
@@ -187,5 +187,20 @@ namespace Wox.Plugin
/// Gets plugin ID that generated this result
///
public string PluginID { get; internal set; }
+
+ ///
+ /// Gets or sets a value indicating whether usage based sorting should be applied to this result.
+ ///
+ public bool DisableUsageBasedScoring { get; set; }
+
+ public int GetSortOrderScore(int selectedItemMultiplier)
+ {
+ if (DisableUsageBasedScoring)
+ {
+ return Metadata.WeightBoost + Score;
+ }
+
+ return Metadata.WeightBoost + Score + (SelectedCount * selectedItemMultiplier);
+ }
}
}
diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
index c2f0698f25..5d32f1d565 100644
--- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
+++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
@@ -62,11 +62,11 @@ public partial class PowerAccent : IDisposable
private void SetEvents()
{
- _keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey) =>
+ _keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey, TriggerKey trigger ) =>
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
- ShowToolbar(letterKey);
+ ShowToolbar(letterKey, trigger);
});
}));
@@ -92,23 +92,15 @@ public partial class PowerAccent : IDisposable
}));
}
- private void ShowToolbar(LetterKey letterKey)
+ private void ShowToolbar(LetterKey letterKey, TriggerKey trigger)
{
_visible = true;
_characters = GetCharacters(letterKey);
_characterDescriptions = GetCharacterDescriptions(_characters);
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
-
- Task.Delay(_settingService.InputTime).ContinueWith(
- t =>
- {
- if (_visible)
- {
- OnChangeDisplay?.Invoke(true, _characters);
- }
- },
- TaskScheduler.FromCurrentSynchronizationContext());
+ OnChangeDisplay?.Invoke(true, _characters);
+ ProcessNextChar(trigger, false);
}
private string[] GetCharacters(LetterKey letterKey)
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
index 93fb5c0230..3d38d20b91 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
@@ -13,7 +13,7 @@
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
KeyboardListener::KeyboardListener() :
- m_toolbarVisible(false), m_triggeredWithSpace(false), m_leftShiftPressed(false), m_rightShiftPressed(false), m_triggeredWithLeftArrow(false), m_triggeredWithRightArrow(false)
+ m_toolbarVisible(false), m_activationKeyHold(false), m_triggeredWithSpace(false), m_leftShiftPressed(false), m_rightShiftPressed(false), m_triggeredWithLeftArrow(false), m_triggeredWithRightArrow(false)
{
s_instance = this;
LoggerHelpers::init_logger(L"PowerAccent", L"PowerAccentKeyboardService", "PowerAccent");
@@ -53,8 +53,8 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
void KeyboardListener::SetShowToolbarEvent(ShowToolbar showToolbarEvent)
{
- m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key) {
- trigger(key);
+ m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key, TriggerKey triggerKey) {
+ trigger(key, triggerKey);
};
}
@@ -152,6 +152,17 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
return false;
}
+ void KeyboardListener::BeginShowToolbar(std::chrono::milliseconds delay, LetterKey key, TriggerKey trigger)
+ {
+ std::unique_lock lock(toolbarMutex);
+ auto result = toolbarCV.wait_for(lock, delay);
+ if (result == std::cv_status::timeout)
+ {
+ m_toolbarVisible = true;
+ m_showToolbarCb(key, trigger);
+ }
+ }
+
bool KeyboardListener::OnKeyDown(KBDLLHOOKSTRUCT info) noexcept
{
auto letterKey = static_cast(info.vkCode);
@@ -199,7 +210,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
}
}
- if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
+ if (!m_toolbarVisible && !m_activationKeyHold && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
{
Logger::debug(L"Show toolbar. Letter: {}, Trigger: {}", letterPressed, triggerPressed);
@@ -207,11 +218,21 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
m_triggeredWithSpace = triggerPressed == VK_SPACE;
m_triggeredWithLeftArrow = triggerPressed == VK_LEFT;
m_triggeredWithRightArrow = triggerPressed == VK_RIGHT;
- m_toolbarVisible = true;
- m_showToolbarCb(letterPressed);
+ m_activationKeyHold = true;
+ m_bothKeysPressed = true;
+ if (toolbarThread != nullptr)
+ {
+ toolbarCV.notify_all();
+ toolbarThread->join();
+ }
+ toolbarThread = std::make_unique(std::bind(&KeyboardListener::BeginShowToolbar, this, m_settings.inputTime, letterPressed,static_cast(triggerPressed)));
}
- if (m_toolbarVisible && triggerPressed)
+ if (m_activationKeyHold && triggerPressed && !m_toolbarVisible)
+ {
+ return true;
+ }
+ else if (m_toolbarVisible && triggerPressed)
{
if (triggerPressed == VK_LEFT)
{
@@ -251,8 +272,9 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
letterPressed = LetterKey::None;
- if (m_toolbarVisible)
+ if (m_toolbarVisible || m_bothKeysPressed)
{
+ m_bothKeysPressed = false;
if (m_stopwatch.elapsed() < m_settings.inputTime)
{
Logger::debug(L"Activation too fast. Do nothing.");
@@ -280,11 +302,18 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
Logger::debug(L"Hide toolbar event and input char");
m_hideToolbarCb(InputType::Char);
-
m_toolbarVisible = false;
}
}
+ auto triggerPressed = info.vkCode;
+
+ if (m_activationKeyHold && (letterPressed == LetterKey::None || (triggerPressed == VK_SPACE || triggerPressed == VK_LEFT || triggerPressed == VK_RIGHT)))
+ {
+ m_activationKeyHold = false;
+ toolbarCV.notify_all();
+ }
+
return false;
}
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
index 61c28e1866..6ec1118775 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
@@ -2,6 +2,7 @@
#include "KeyboardListener.g.h"
#include
+#include
#include
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
@@ -44,6 +45,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
+ void BeginShowToolbar(std::chrono::milliseconds delay, LetterKey key, TriggerKey trigger);
bool OnKeyDown(KBDLLHOOKSTRUCT info) noexcept;
bool OnKeyUp(KBDLLHOOKSTRUCT info) noexcept;
bool IsSuppressedByGameMode();
@@ -51,9 +53,14 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
static inline KeyboardListener* s_instance;
HHOOK s_llKeyboardHook = nullptr;
- bool m_toolbarVisible;
+ std::atomic m_toolbarVisible;
+ bool m_activationKeyHold;
+ bool m_bothKeysPressed = false;
+ std::unique_ptr toolbarThread;
+ std::mutex toolbarMutex;
+ std::condition_variable toolbarCV;
PowerAccentSettings m_settings;
- std::function m_showToolbarCb;
+ std::function m_showToolbarCb;
std::function m_hideToolbarCb;
std::function m_nextCharCb;
std::function m_isLanguageLetterCb;
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
index 9bc8448c22..9fc0f42e42 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
@@ -67,7 +67,7 @@ namespace PowerToys
Char
};
- [version(1.0), uuid(37197089-5438-4479-af57-30ab3f3c8be4)] delegate void ShowToolbar(LetterKey key);
+ [version(1.0), uuid(37197089-5438-4479-af57-30ab3f3c8be4)] delegate void ShowToolbar(LetterKey key, TriggerKey trigger);
[version(1.0), uuid(8eb79d6b-1826-424f-9fbc-af21ae19725e)] delegate void HideToolbar(InputType inputType);
[version(1.0), uuid(db72d45c-a5a2-446f-bdc1-506e9121764a)] delegate void NextChar(TriggerKey inputSpace, boolean shiftPressed);
[version(1.0), uuid(20be2919-2b91-4313-b6e0-4c3484fe91ef)] delegate void IsLanguageLetter(LetterKey key, [out] boolean* result);
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json b/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json
new file mode 100644
index 0000000000..41ceb9414a
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json
@@ -0,0 +1,40 @@
+{
+ "configVersion": 3,
+ "entries": [
+ {
+ "Fuzzer": {
+ "$type": "libfuzzer",
+ "FuzzingHarnessExecutableName": "PowerRename.FuzzingTest.exe"
+ },
+ "adoTemplate": {
+ // supply the values appropriate to your
+ // project, where bugs will be filed
+ "org": "microsoft",
+ "project": "OS",
+ "AssignedTo": "leilzh@microsoft.com",
+ "AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\SALT",
+ "IterationPath": "OS\\Future"
+ },
+ "jobNotificationEmail": "PowerToys@microsoft.com",
+ "skip": false,
+ "rebootAfterSetup": false,
+ "oneFuzzJobs": [
+ // at least one job is required
+ {
+ "projectName": "PowerToys.PowerRename",
+ "targetName": "PowerRename_Fuzzer"
+ }
+ ],
+ "jobDependencies": [
+ // this should contain, at minimum,
+ // the DLL and PDB files
+ // you will need to add any other files required
+ // (globs are supported)
+ "PowerRename.FuzzingTest.exe",
+ "PowerRename.FuzzingTest.pdb",
+ "PowerRename.FuzzingTest.lib",
+ "clang_rt.asan_dynamic-x86_64.dll"
+ ]
+ }
+ ]
+}
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp
new file mode 100644
index 0000000000..5a060ad3fb
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp
@@ -0,0 +1,67 @@
+// Test.cpp : This file contains the 'main' function. Program execution begins and ends there.
+//
+
+#include
+#include
+#include
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ if (size < 6)
+ return 0;
+
+ size_t offset = 0;
+
+ size_t input_len = size / 3;
+ size_t find_len = size / 3;
+ size_t replace_len = size - input_len - find_len;
+
+ auto read_wstring = [&](size_t len) -> std::wstring {
+ std::wstring result;
+ if (offset + len > size)
+ len = size - offset;
+
+ result.assign(reinterpret_cast(data + offset), len / sizeof(wchar_t));
+ offset += len;
+ return result;
+ };
+
+ std::wstring input = read_wstring(input_len);
+ std::wstring find = read_wstring(find_len);
+ std::wstring replace = read_wstring(replace_len);
+
+ if (find.empty() || replace.empty())
+ return 0;
+
+ CComPtr renamer;
+ CPowerRenameRegEx::s_CreateInstance(&renamer);
+
+ renamer->PutFlags(UseRegularExpressions | CaseSensitive);
+
+ renamer->PutSearchTerm(find.c_str());
+ renamer->PutReplaceTerm(replace.c_str());
+
+ PWSTR result = nullptr;
+ unsigned long index = 0;
+ HRESULT hr = renamer->Replace(input.c_str(), &result, index);
+ if (SUCCEEDED(hr) && result != nullptr)
+ {
+ CoTaskMemFree(result);
+ }
+
+ return 0;
+}
+
+#ifndef DISABLE_FOR_FUZZING
+
+int main(int argc, char** argv)
+{
+ const char8_t raw[] = u8"test_string";
+
+ std::vector data(reinterpret_cast(raw), reinterpret_cast(raw) + sizeof(raw) - 1);
+
+ LLVMFuzzerTestOneInput(data.data(), data.size());
+ return 0;
+}
+
+#endif
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters
new file mode 100644
index 0000000000..21d23a2ee7
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters
@@ -0,0 +1,25 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
new file mode 100644
index 0000000000..6dc9ddcdb2
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
@@ -0,0 +1,107 @@
+
+
+
+
+ 17.0
+ Win32Proj
+ {2694e2fb-dcd5-4bff-a418-b6c3c7ce3b8e}
+ Test
+ 10.0.22621.0
+ PowerRename.FuzzingTest
+
+
+
+ Application
+ false
+ v143
+ Unicode
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(VCToolsInstallDir)\lib\$(Platform)
+ ..\..\..\..\$(Platform)\$(Configuration)\tests\PowerRename.FuzzTests\
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;DISABLE_FOR_FUZZING;%(PreprocessorDefinitions);_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
+ true
+ NotUsing
+ /fsanitize=address /fsanitize-coverage=inline-8bit-counters /fsanitize-coverage=edge /fsanitize-coverage=trace-cmp /fsanitize-coverage=trace-div %(AdditionalOptions)
+ MultiThreaded
+ stdcpplatest
+ ..\;..\lib\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Console
+ true
+ true
+ true
+ legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+ xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
+ Copy the required ASan runtime DLL to the output directory.
+
+
+
+
+ Level3
+ true
+ true
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ NotUsing
+ ..\;..\lib\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Console
+ $(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+ {51920f1f-c28c-4adf-8660-4238766796c2}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+ 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}.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
index 40bbb7a682..806da3904b 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
@@ -1,6 +1,6 @@
-
+
@@ -207,7 +207,7 @@
-
+
@@ -221,8 +221,8 @@
-
-
+
+
diff --git a/src/modules/powerrename/PowerRenameUILib/packages.config b/src/modules/powerrename/PowerRenameUILib/packages.config
index c8aa0dc6df..77b75fad7e 100644
--- a/src/modules/powerrename/PowerRenameUILib/packages.config
+++ b/src/modules/powerrename/PowerRenameUILib/packages.config
@@ -6,5 +6,5 @@
-
+
\ No newline at end of file
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
index 0df3a5c0f5..20009533c1 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
@@ -192,12 +192,70 @@ namespace RegistryPreviewUILib
// reload the current Registry file and update the toolbar accordingly.
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true);
+ // disable the Save button as it's a new file
saveButton.IsEnabled = false;
// restore the TextChanged handler
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
}
+ ///
+ /// Resets the editor content
+ ///
+ private async void NewButton_Click(object sender, RoutedEventArgs e)
+ {
+ // Check to see if the current file has been saved
+ if (saveButton.IsEnabled)
+ {
+ ContentDialog contentDialog = new ContentDialog()
+ {
+ Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
+ Content = resourceLoader.GetString("YesNoCancelDialogContent"),
+ PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
+ SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
+ CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
+ DefaultButton = ContentDialogButton.Primary,
+ };
+
+ // Use this code to associate the dialog to the appropriate AppWindow by setting
+ // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
+ if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
+ {
+ contentDialog.XamlRoot = this.Content.XamlRoot;
+ }
+
+ ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
+ switch (contentDialogResult)
+ {
+ case ContentDialogResult.Primary:
+ // Save, then continue the file open
+ SaveFile();
+ break;
+ case ContentDialogResult.Secondary:
+ // Don't save and continue the file open!
+ saveButton.IsEnabled = false;
+ break;
+ default:
+ // Don't open the new file!
+ return;
+ }
+ }
+
+ // mute the TextChanged handler to make for clean UI
+ MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
+
+ // reset editor, file info and ui.
+ _appFileName = string.Empty;
+ ResetEditorAndFile();
+
+ // restore the TextChanged handler
+ MonacoEditor.TextChanged += MonacoEditor_TextChanged;
+
+ // disable buttons that do not make sense
+ saveButton.IsEnabled = false;
+ refreshButton.IsEnabled = false;
+ }
+
///
/// Opens the Registry Editor; UAC is handled by the request to open
///
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
index 819fee949a..3ec275abf8 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
@@ -22,6 +22,8 @@ namespace RegistryPreviewUILib
{
public sealed partial class RegistryPreviewMainPage : Page
{
+ private const string NEWFILEHEADER = "Windows Registry Editor Version 5.00\r\n\r\n";
+
private static SemaphoreSlim _dialogSemaphore = new(1);
private string lastKeyPath;
@@ -77,6 +79,9 @@ namespace RegistryPreviewUILib
}
catch
{
+ // Set default value for empty opening
+ await MonacoEditor.SetTextAsync(NEWFILEHEADER);
+
// restore TextChanged handler to make for clean UI
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
@@ -167,6 +172,25 @@ namespace RegistryPreviewUILib
ChangeCursor(gridPreview, false);
}
+ private async void ResetEditorAndFile()
+ {
+ // Disable parts of the UI that can cause trouble when loading
+ ChangeCursor(gridPreview, true);
+
+ // clear the treeView and dataGrid no matter what
+ treeView.RootNodes.Clear();
+ ClearTable();
+
+ // update the current window's title with the current filename
+ _updateWindowTitleFunction(string.Empty);
+
+ // Set default value for empty opening
+ await MonacoEditor.SetTextAsync(NEWFILEHEADER);
+
+ // Reset the cursor but leave editor disabled as no content got loaded
+ ChangeCursor(gridPreview, false);
+ }
+
///
/// Parses the text that is passed in, which should be the same text that's in editor
///
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
index a48222e9aa..18225902e3 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
@@ -55,6 +55,15 @@
HorizontalAlignment="Left"
DefaultLabelPosition="Right">
+
+
+
+
+
Copy value with key path
Like "Copy item"
+
+ New
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
index 406f67c2a4..9102609b6b 100644
--- a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
@@ -4,9 +4,7 @@
using System;
using System.IO;
-using System.IO.Abstractions;
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
@@ -46,17 +44,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
{
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText());
-
- if (Hotkey == null)
- {
- Hotkey = DefaultHotkeyValue;
- }
}
}
catch (Exception)
{
- Hotkey = DefaultHotkeyValue;
}
+
+ Hotkey ??= DefaultHotkeyValue;
}
}
}
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
index 0d3fc918d6..b82ef56888 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
@@ -32,8 +32,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
VisibleColorFormats.Add("HSI", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HSI")));
VisibleColorFormats.Add("HWB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HWB")));
VisibleColorFormats.Add("NCol", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("NCol")));
- VisibleColorFormats.Add("CIELAB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
VisibleColorFormats.Add("CIEXYZ", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIEXYZ")));
+ VisibleColorFormats.Add("CIELAB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
+ VisibleColorFormats.Add("Oklab", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Oklab")));
+ VisibleColorFormats.Add("Oklch", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Oklch")));
VisibleColorFormats.Add("VEC4", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("VEC4")));
VisibleColorFormats.Add("Decimal", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Decimal")));
VisibleColorFormats.Add("HEX Int", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HEX Int")));
diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
index 09ef003e88..7e57f5a730 100644
--- a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
+++ b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
@@ -80,5 +80,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations
/// Color presentation as an 8-digit hexadecimal integer (0xFFFFFFFF)
///
HexInteger = 13,
+
+ ///
+ /// Color representation as CIELCh color space (L[0..100], C[0..230], h[0°..360°])
+ ///
+ CIELCh = 14,
+
+ ///
+ /// Color representation as Oklab color space (L[0..1], a[-0.5..0.5], b[-0.5..0.5])
+ ///
+ Oklab = 15,
+
+ ///
+ /// Color representation as Oklch color space (L[0..1], C[0..0.5], h[0°..360°])
+ ///
+ Oklch = 16,
}
}
diff --git a/src/settings-ui/Settings.UI.Library/ImageSize.cs b/src/settings-ui/Settings.UI.Library/ImageSize.cs
index 017f665820..39b712d67f 100644
--- a/src/settings-ui/Settings.UI.Library/ImageSize.cs
+++ b/src/settings-ui/Settings.UI.Library/ImageSize.cs
@@ -8,11 +8,12 @@ using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
+using ManagedCommon;
using Settings.UI.Library.Resources;
namespace Microsoft.PowerToys.Settings.UI.Library;
-public partial class ImageSize : INotifyPropertyChanged
+public partial class ImageSize : INotifyPropertyChanged, IHasId
{
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
index 32f0ee488e..ea6d1c9f22 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
@@ -32,7 +32,8 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Description}"
TextTrimming="CharacterEllipsis"
- TextWrapping="NoWrap" />
+ TextWrapping="NoWrap"
+ ToolTipService.ToolTip="{x:Bind Description}" />
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
index 76681d8b46..475d399674 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
@@ -47,12 +47,17 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
new ColorFormatParameter() { Parameter = "%In", Description = resourceLoader.GetString("Help_intensity") },
new ColorFormatParameter() { Parameter = "%Hn", Description = resourceLoader.GetString("Help_hueNat") },
new ColorFormatParameter() { Parameter = "%Ll", Description = resourceLoader.GetString("Help_lightnessNat") },
- new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
new ColorFormatParameter() { Parameter = "%Va", Description = resourceLoader.GetString("Help_value") },
new ColorFormatParameter() { Parameter = "%Wh", Description = resourceLoader.GetString("Help_whiteness") },
new ColorFormatParameter() { Parameter = "%Bn", Description = resourceLoader.GetString("Help_blackness") },
- new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityA") },
- new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityB") },
+ new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
+ new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityACIE") },
+ new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityBCIE") },
+ new ColorFormatParameter() { Parameter = "%Lo", Description = resourceLoader.GetString("Help_lightnessOklab") },
+ new ColorFormatParameter() { Parameter = "%Oa", Description = resourceLoader.GetString("Help_chromaticityAOklab") },
+ new ColorFormatParameter() { Parameter = "%Ob", Description = resourceLoader.GetString("Help_chromaticityBOklab") },
+ new ColorFormatParameter() { Parameter = "%Oc", Description = resourceLoader.GetString("Help_chromaOklch") },
+ new ColorFormatParameter() { Parameter = "%Oh", Description = resourceLoader.GetString("Help_hueOklch") },
new ColorFormatParameter() { Parameter = "%Xv", Description = resourceLoader.GetString("Help_X_value") },
new ColorFormatParameter() { Parameter = "%Yv", Description = resourceLoader.GetString("Help_Y_value") },
new ColorFormatParameter() { Parameter = "%Zv", Description = resourceLoader.GetString("Help_Z_value") },
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index a95cdb86cd..694603707c 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -1660,11 +1660,11 @@ Made with 💗 by Microsoft and the PowerToys community.
blackness
-
- chromaticityA
+
+ chromaticity A (CIE Lab)
-
- chromaticityB
+
+ chromaticity B (CIE Lab)
X value
@@ -4460,7 +4460,7 @@ Activate by holding the key for the character you want to add an accent to, then
Commonly used variables
New+ commonly used variables header in the flyout info card
-
+
Year, represented by a full four or five digits, depending on the calendar used.
New+ description of the year $YYYY variable - casing of $YYYY is important
@@ -4999,4 +4999,25 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
Go to Command Palette settings to customize the activation shortcut.
+
+ chroma (CIE LCh)
+
+
+ hue (CIE LCh)
+
+
+ lightness (Oklab/Oklch)
+
+
+ chromaticity A (Oklab)
+
+
+ chromaticity B (Oklab)
+
+
+ chroma (Oklch)
+
+
+ hue (Oklch)
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
index 511a9cb36d..de70cd3311 100644
--- a/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
@@ -67,6 +67,7 @@ public partial class ImageResizerViewModel : Observable
try
{
Settings = _settingsUtils.GetSettings(ModuleName);
+ IdRecoveryHelper.RecoverInvalidIds(Settings.Properties.ImageresizerSizes.Value);
}
catch (Exception e)
{
diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1
new file mode 100644
index 0000000000..28e6939760
--- /dev/null
+++ b/tools/build/build-installer.ps1
@@ -0,0 +1,122 @@
+<#
+.SYNOPSIS
+Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
+
+.DESCRIPTION
+This script automates the end-to-end build and packaging process for PowerToys, including:
+- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.)
+- Cleaning up old output
+- Signing generated .msix packages
+- Building the WiX-based MSI and bootstrapper installers
+
+It is designed to work in local development.
+
+.PARAMETER Platform
+Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'.
+
+.PARAMETER Configuration
+Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
+
+.EXAMPLE
+.\build-installer.ps1
+Runs the installer build pipeline for ARM64 Release (default).
+
+.EXAMPLE
+.\build-installer.ps1 -Platform x64 -Configuration Release
+Runs the pipeline for x64 Debug.
+
+.NOTES
+- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment.
+- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
+- Generated MSIX files will be signed using cert-sign-package.ps1.
+- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
+- First time run need admin permission to trust the certificate.
+- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup
+ relative to the solution root directory.
+- The installer can't be run right after the build, I need to copy it to another file before it can be run.
+#>
+
+
+param (
+ [string]$Platform = 'arm64',
+ [string]$Configuration = 'Release'
+)
+
+$repoRoot = Resolve-Path "$PSScriptRoot\..\.."
+Set-Location $repoRoot
+
+function RunMSBuild {
+ param (
+ [string]$Solution,
+ [string]$ExtraArgs
+ )
+
+ $base = @(
+ $Solution
+ "/p:Platform=`"$Platform`""
+ "/p:Configuration=$Configuration"
+ '/verbosity:normal'
+ '/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
+ '/nologo'
+ )
+
+ $cmd = $base + ($ExtraArgs -split ' ')
+ Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' '))
+ & msbuild.exe @cmd
+
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs)
+ exit $LASTEXITCODE
+ }
+
+}
+
+function RestoreThenBuild {
+ param ([string]$Solution)
+
+ # 1) restore
+ RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true'
+ # 2) build -------------------------------------------------
+ RunMSBuild $Solution '/m'
+}
+
+Write-Host ("Make sure wix is installed and available")
+& "$PSScriptRoot\ensure-wix.ps1"
+
+Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration)
+Write-Host ''
+
+$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
+
+if (Test-Path $cmdpalOutputPath) {
+ Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
+ Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
+}
+
+RestoreThenBuild '.\PowerToys.sln'
+
+$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
+$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
+Select-Object -ExpandProperty FullName
+
+if ($msixFiles.Count) {
+ Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
+ & "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles
+}
+else {
+ Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
+}
+
+RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln'
+RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln'
+
+Write-Host '[CLEAN] installer (keep *.exe)'
+git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
+
+RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true'
+
+RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true'
+
+RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true'
+
+Write-Host '[PIPELINE] Completed'
\ No newline at end of file
diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1
new file mode 100644
index 0000000000..ed7031c1e9
--- /dev/null
+++ b/tools/build/cert-management.ps1
@@ -0,0 +1,155 @@
+<#
+.SYNOPSIS
+Ensures a code signing certificate exists and is trusted in all necessary certificate stores.
+
+.DESCRIPTION
+This script provides two functions:
+
+1. EnsureCertificate:
+ - Searches for an existing code signing certificate by subject name.
+ - If not found, creates a new self-signed certificate.
+ - Exports the certificate and attempts to import it into:
+ - CurrentUser\TrustedPeople
+ - CurrentUser\Root
+ - LocalMachine\Root (admin privileges may be required)
+
+2. ImportAndVerifyCertificate:
+ - Imports a `.cer` file into the specified certificate store if not already present.
+ - Verifies the certificate is successfully imported by checking thumbprint.
+
+This is useful in build or signing pipelines to ensure a valid and trusted certificate is available before signing MSIX or executable files.
+
+.PARAMETER certSubject
+The subject name of the certificate to search for or create. Default is:
+"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
+
+.PARAMETER cerPath
+(ImportAndVerifyCertificate only) The file path to a `.cer` certificate file to import.
+
+.PARAMETER storePath
+(ImportAndVerifyCertificate only) The destination certificate store path (e.g. Cert:\CurrentUser\Root).
+
+.EXAMPLE
+$cert = EnsureCertificate
+
+Ensures the default certificate exists and is trusted, and returns the certificate object.
+
+.EXAMPLE
+ImportAndVerifyCertificate -cerPath "$env:TEMP\temp_cert.cer" -storePath "Cert:\CurrentUser\Root"
+
+Imports a certificate into the CurrentUser Root store and verifies its presence.
+
+.NOTES
+- For full trust, administrative privileges may be needed to import into LocalMachine\Root.
+- Certificates are created using RSA and SHA256 and marked as CodeSigningCert.
+#>
+
+function ImportAndVerifyCertificate {
+ param (
+ [string]$cerPath,
+ [string]$storePath
+ )
+
+ $thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint
+
+ $existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
+ if ($existingCert) {
+ Write-Host "Certificate already exists in $storePath"
+ return $true
+ }
+
+ try {
+ $null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
+ } catch {
+ Write-Warning "Failed to import certificate to $storePath : $_"
+ return $false
+ }
+
+ $imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
+ if ($imported) {
+ Write-Host "Certificate successfully imported to $storePath"
+ return $true
+ } else {
+ Write-Warning "Certificate not found in $storePath after import"
+ return $false
+ }
+}
+
+function EnsureCertificate {
+ param (
+ [string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
+ )
+
+ $cert = Get-ChildItem -Path Cert:\CurrentUser\My |
+ Where-Object { $_.Subject -eq $certSubject } |
+ Sort-Object NotAfter -Descending |
+ Select-Object -First 1
+
+ if (-not $cert) {
+ Write-Host "Certificate not found. Creating a new one..."
+
+ $cert = New-SelfSignedCertificate -Subject $certSubject `
+ -CertStoreLocation "Cert:\CurrentUser\My" `
+ -KeyAlgorithm RSA `
+ -Type CodeSigningCert `
+ -HashAlgorithm SHA256
+
+ if (-not $cert) {
+ Write-Error "Failed to create a new certificate."
+ return $null
+ }
+
+ Write-Host "New certificate created with thumbprint: $($cert.Thumbprint)"
+ }
+ else {
+ Write-Host "Using existing certificate with thumbprint: $($cert.Thumbprint)"
+ }
+
+ $cerPath = "$env:TEMP\temp_cert.cer"
+ [void](Export-Certificate -Cert $cert -FilePath $cerPath -Force)
+
+ if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) { return $null }
+ if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) { return $null }
+ if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) {
+ Write-Warning "Failed to import to LocalMachine\Root (admin may be required)"
+ return $null
+ }
+
+ return $cert
+}
+
+function Export-CertificateFiles {
+ param (
+ [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
+ [string]$CerPath,
+ [string]$PfxPath,
+ [securestring]$PfxPassword
+ )
+
+ if (-not $Certificate) {
+ Write-Error "No certificate provided to export."
+ return
+ }
+
+ if ($CerPath) {
+ try {
+ Export-Certificate -Cert $Certificate -FilePath $CerPath -Force | Out-Null
+ Write-Host "Exported CER to: $CerPath"
+ } catch {
+ Write-Warning "Failed to export CER file: $_"
+ }
+ }
+
+ if ($PfxPath -and $PfxPassword) {
+ try {
+ Export-PfxCertificate -Cert $Certificate -FilePath $PfxPath -Password $PfxPassword -Force | Out-Null
+ Write-Host "Exported PFX to: $PfxPath"
+ } catch {
+ Write-Warning "Failed to export PFX file: $_"
+ }
+ }
+
+ if (-not $CerPath -and -not $PfxPath) {
+ Write-Warning "No output path specified. Nothing was exported."
+ }
+}
\ No newline at end of file
diff --git a/tools/build/cert-sign-package.ps1 b/tools/build/cert-sign-package.ps1
new file mode 100644
index 0000000000..8bb57762a5
--- /dev/null
+++ b/tools/build/cert-sign-package.ps1
@@ -0,0 +1,29 @@
+param (
+ [string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
+ [string[]]$TargetPaths = "C:\PowerToys\ARM64\Release\WinUI3Apps\CmdPal\AppPackages\Microsoft.CmdPal.UI_0.0.1.0_Test\Microsoft.CmdPal.UI_0.0.1.0_arm64.msix"
+)
+
+. "$PSScriptRoot\cert-management.ps1"
+$cert = EnsureCertificate -certSubject $certSubject
+
+if (-not $cert) {
+ Write-Error "Failed to prepare certificate."
+ exit 1
+}
+
+Write-Host "Certificate ready: $($cert.Thumbprint)"
+
+if (-not $TargetPaths -or $TargetPaths.Count -eq 0) {
+ Write-Error "No target files provided to sign."
+ exit 1
+}
+
+foreach ($filePath in $TargetPaths) {
+ if (-not (Test-Path $filePath)) {
+ Write-Warning "Skipping: File does not exist - $filePath"
+ continue
+ }
+
+ Write-Host "Signing: $filePath"
+ & signtool sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com "$filePath"
+}
\ No newline at end of file
diff --git a/tools/build/ensure-wix.ps1 b/tools/build/ensure-wix.ps1
new file mode 100644
index 0000000000..988d382f07
--- /dev/null
+++ b/tools/build/ensure-wix.ps1
@@ -0,0 +1,71 @@
+<#
+.SYNOPSIS
+ Ensure WiX Toolset 3.14 (build 3141) is installed and ready to use.
+
+.DESCRIPTION
+ - Skips installation if the toolset is already installed (unless -Force is used).
+ - Otherwise downloads the official installer and binaries, verifies SHA-256, installs silently,
+ and copies wix.targets into the installation directory.
+.PARAMETER Force
+ Forces reinstallation even if the toolset is already detected.
+.PARAMETER InstallDir
+ The target installation path. Default is 'C:\Program Files (x86)\WiX Toolset v3.14'.
+.EXAMPLE
+ .\EnsureWix.ps1 # Ensure WiX is installed
+ .\EnsureWix.ps1 -Force # Force reinstall
+#>
+[CmdletBinding()]
+param(
+ [switch]$Force,
+ [string]$InstallDir = 'C:\Program Files (x86)\WiX Toolset v3.14'
+)
+
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+
+# Download URLs and expected SHA-256 hashes
+$WixDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe'
+$WixBinariesDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip'
+$InstallerHashExpected = '6BF6D03D6923D9EF827AE1D943B90B42B8EBB1B0F68EF6D55F868FA34C738A29'
+$BinariesHashExpected = '6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31'
+
+# Check if WiX is already installed
+$candlePath = Join-Path $InstallDir 'bin\candle.exe'
+if (-not $Force -and (Test-Path $candlePath)) {
+ Write-Host "WiX Toolset is already installed at `"$InstallDir`". Skipping installation."
+ return
+}
+
+# Temp file paths
+$tmpDir = [IO.Path]::GetTempPath()
+$installer = Join-Path $tmpDir 'wix314.exe'
+$binariesZip = Join-Path $tmpDir 'wix314-binaries.zip'
+
+# Download installer and binaries
+Write-Host 'Downloading WiX installer...'
+Invoke-WebRequest -Uri $WixDownloadUrl -OutFile $installer -UseBasicParsing
+Write-Host 'Downloading WiX binaries...'
+Invoke-WebRequest -Uri $WixBinariesDownloadUrl -OutFile $binariesZip -UseBasicParsing
+
+# Verify SHA-256 hashes
+Write-Host 'Verifying installer hash...'
+if ((Get-FileHash -Algorithm SHA256 $installer).Hash -ne $InstallerHashExpected) {
+ throw 'wix314.exe SHA256 hash mismatch'
+}
+Write-Host 'Verifying binaries hash...'
+if ((Get-FileHash -Algorithm SHA256 $binariesZip).Hash -ne $BinariesHashExpected) {
+ throw 'wix314-binaries.zip SHA256 hash mismatch'
+}
+
+# Perform silent installation
+Write-Host 'Installing WiX Toolset silently...'
+Start-Process -FilePath $installer -ArgumentList '/install','/quiet' -Wait
+
+# Extract binaries and copy wix.targets
+$expandDir = Join-Path $tmpDir 'wix-binaries'
+if (Test-Path $expandDir) { Remove-Item $expandDir -Recurse -Force }
+Expand-Archive -Path $binariesZip -DestinationPath $expandDir -Force
+Copy-Item -Path (Join-Path $expandDir 'wix.targets') `
+ -Destination (Join-Path $InstallDir 'wix.targets') -Force
+
+Write-Host "WiX Toolset has been successfully installed at: $InstallDir"