Compare commits

..

5 Commits

266 changed files with 4059 additions and 9330 deletions

View File

@@ -1,5 +1,5 @@
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
properties:
resources:
- resource: Microsoft.Windows.Settings/WindowsSettings
@@ -13,11 +13,11 @@ properties:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2026 Enterprise (Any edition will work)
description: Install Visual Studio 2022 Enterprise (Any edition will work)
# Requires elevation for the set operation
securityContext: elevated
settings:
id: Microsoft.VisualStudio.Enterprise
id: Microsoft.VisualStudio.2022.Enterprise
source: winget
- resource: Microsoft.VisualStudio.DSC/VSComponents
dependsOn:
@@ -29,7 +29,7 @@ properties:
securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Enterprise
channelId: VisualStudio.18.Release
channelId: VisualStudio.17.Release
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
configurationVersion: 0.2.0

View File

@@ -1,5 +1,5 @@
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
properties:
resources:
- resource: Microsoft.Windows.Settings/WindowsSettings
@@ -13,11 +13,11 @@ properties:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2026 Professional (Any edition will work)
description: Install Visual Studio 2022 Professional (Any edition will work)
# Requires elevation for the set operation
securityContext: elevated
settings:
id: Microsoft.VisualStudio.Professional
id: Microsoft.VisualStudio.2022.Professional
source: winget
- resource: Microsoft.VisualStudio.DSC/VSComponents
dependsOn:
@@ -29,7 +29,7 @@ properties:
securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Professional
channelId: VisualStudio.18.Release
channelId: VisualStudio.17.Release
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
configurationVersion: 0.2.0

View File

@@ -1,5 +1,5 @@
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
properties:
resources:
- resource: Microsoft.Windows.Settings/WindowsSettings
@@ -13,11 +13,11 @@ properties:
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsPackage
directives:
description: Install Visual Studio 2026 Community (Any edition will work)
description: Install Visual Studio 2022 Community (Any edition will work)
# Requires elevation for the set operation
securityContext: elevated
settings:
id: Microsoft.VisualStudio.Community
id: Microsoft.VisualStudio.2022.Community
source: winget
- resource: Microsoft.VisualStudio.DSC/VSComponents
dependsOn:
@@ -29,7 +29,7 @@ properties:
securityContext: elevated
settings:
productId: Microsoft.VisualStudio.Product.Community
channelId: VisualStudio.18.Release
channelId: VisualStudio.17.Release
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
configurationVersion: 0.2.0

View File

@@ -38,7 +38,6 @@ Gbps
gcode
Heatshrink
Mbits
Kbits
MBs
mkv
msix
@@ -98,7 +97,6 @@ Yubico
Perplexity
Groq
svgl
devhome
# KEYS
@@ -324,7 +322,6 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
MEMORYSTATUSEX
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
@@ -345,7 +342,3 @@ reportbug
#ffmpeg
crf
nostdin
# Performance counter keys
engtype
Nonpaged

View File

@@ -192,7 +192,6 @@ ycv
yeelam
Yuniardi
yuyoyuppe
zadjii
Zeol
Zhao
Zhaopeng
@@ -229,7 +228,6 @@ regedit
roslyn
Skia
Spotify
taskmgr
tldr
Vanara
wangyi
@@ -245,3 +243,4 @@ xamlstyler
Xavalon
Xbox
Youdao
zadjii

View File

@@ -111,7 +111,6 @@
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/.*\.TestData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/Text/.*\.cs$
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$

View File

@@ -197,7 +197,6 @@ Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
carlos
Carlseibert
CAtl
caub
@@ -218,7 +217,6 @@ certmgr
cfp
CHANGECBCHAIN
changecursor
chatasweetie
checkmarks
CHILDACTIVATE
CHILDWINDOW
@@ -1529,7 +1527,6 @@ randi
RAquadrant
rasterization
Rasterize
rasterizing
RAWINPUTDEVICE
RAWINPUTHEADER
RAWMODE
@@ -2244,7 +2241,6 @@ YSpeed
YStr
YTimer
YVIRTUALSCREEN
zamora
ZEROINIT
zonability
zonable

View File

@@ -17,10 +17,10 @@ $nonDirectoryAssetsItems = Get-ChildItem $targetAssetsDir -Attributes !Directory
$directoryAssetsItems = Get-ChildItem $targetAssetsDir -Attributes Directory
if ($directoryAssetsItems.Count -le 0) {
Write-Host -ForegroundColor Red "ERROR: No directories detected in " $nonDirectoryAssetsItems ". Are you sure this is the right path?`r`n"
Write-Host -ForegroundColor Red "No directories detected in " $nonDirectoryAssetsItems ". Are you sure this is the right path?`r`n"
$totalFailures++;
} elseif ($nonDirectoryAssetsItems.Count -gt 0) {
Write-Host -ForegroundColor Red "ERROR: Detected " $nonDirectoryAssetsItems " files in " $targetAssetsDir ". Each application should use a named subdirectory for assets.`r`n"
Write-Host -ForegroundColor Red "Detected " $nonDirectoryAssetsItems " files in " $targetAssetsDir "`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "Only directories detected in " $targetAssetsDir "`r`n"
@@ -29,7 +29,7 @@ if ($directoryAssetsItems.Count -le 0) {
# Make sure there's no resources.pri file. Each application should use a different name for their own resources file path.
$resourcesPriFiles = Get-ChildItem $targetDir -Filter resources.pri
if ($resourcesPriFiles.Count -gt 0) {
Write-Host -ForegroundColor Red "ERROR: Detected a resources.pri file in " $targetDir ". Each application should use a unique name for its resources file.`r`n"
Write-Host -ForegroundColor Red "Detected a resources.pri file in " $targetDir "`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "No resources.pri file detected in " $targetDir "`r`n"
@@ -38,7 +38,7 @@ if ($resourcesPriFiles.Count -gt 0) {
# Each application should have their XAML files in their own paths to avoid these conflicts.
$resourcesPriFiles = Get-ChildItem $targetDir -Filter *.xbf
if ($resourcesPriFiles.Count -gt 0) {
Write-Host -ForegroundColor Red "ERROR: Detected a .xbf file in " $targetDir ". Ensure all XAML files are placed in a subdirectory in each application.`r`n"
Write-Host -ForegroundColor Red "Detected a .xbf file in " $targetDir "`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "No .xbf files detected in " $targetDir "`r`n"

View File

@@ -64,6 +64,7 @@
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<BuildStlModules>false</BuildStlModules>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<!-- TODO: _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING for compatibility with VS 17.8. Check if we can remove. -->
<PreprocessorDefinitions>_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<!-- CLR + CFG are not compatible >:{ -->

View File

@@ -4,6 +4,10 @@
<Platform Name="x64" />
</Configurations>
<Folder Name="/common/">
<Project Path="src/common/AllExperiments/AllExperiments.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/common/CalculatorEngineCommon/CalculatorEngineCommon.vcxproj" Id="2cf78cf7-8feb-4be1-9591-55fa25b48fc6" />
<Project Path="src/common/Common.Search/Common.Search.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
@@ -215,10 +219,6 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -103,38 +103,10 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
</details>
## ✨ What's new
**Version 0.97.2 (Feb 2026)**
**Version 0.97.1 (January 2026)**
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
## Advanced Paste
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
## Color Picker
- #45367 Fixed contrast issue in Color picker UI.
## Command Palette
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
## Cursor Wrap
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
## Image Resizer
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
## LightSwitch
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
## Workspaces
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
## Quick Access and Measure Tool
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
**Version 0.97.1 (January 2026)**
**Highlights**
### Advanced Paste

View File

@@ -88,7 +88,7 @@
### Building PowerToys Locally
#### One stop script for building installer
1. Open `Developer PowerShell for VS`.
1. Open `Developer Powershell for VS 2022` or `Developer PowerShell for VS` for VS 2026.
2. Run tools\build\build-installer.ps1
> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages.
@@ -109,7 +109,7 @@ dotnet tool install --global wix --version 5.0.2
##### From the command line
1. From the start menu, open a `Developer Command Prompt for VS`
1. From the start menu, open a `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
1. Ensure `nuget.exe` is in your `%path%`
1. In the repo root, run these commands:
@@ -140,7 +140,7 @@ If you prefer, you can alternatively build prerequisite projects for the install
The resulting installer will be available in the `installer\PowerToysSetupVNext\x64\Release\` folder.
To build the installer from the command line, run `Developer Command Prompt for VS` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
To build the installer from the command line, run `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS` in admin mode and execute the following commands. The generated installer package will be located at `\installer\PowerToysSetupVNext\{platform}\Release\MachineSetup`.
```
git clean -xfd -e *exe -- .\installer\

View File

@@ -15,7 +15,7 @@ Before you can start debugging PowerToys, you need to set up your development en
You can build the entire solution from the command line, which is sometimes faster than building within Visual Studio:
1. Open `Developer Command Prompt for VS`
1. Open `Developer Command Prompt for VS 2022` or `Developer Command Prompt for VS`
2. Navigate to the repository root directory
3. Run the following command(don't forget to set the correct platform):
```pwsh
@@ -105,7 +105,7 @@ If you encounter build errors about missing image files (e.g., `.png`, `.ico`, o
1. **Clean the solution in Visual Studio**: Build > Clean Solution
Or from the command line (`Developer Command Prompt for VS`):
Or from the command line (Developer Command Prompt for VS 2022 or Developer Command Prompt for VS):
```pwsh
msbuild PowerToys.slnx /t:Clean /p:Platform=x64 /p:Configuration=Debug
```

View File

@@ -15,27 +15,9 @@ VS Code extensions Needed:
---
## Building in VS Code
### Configure Developer PowerShell for VS for more convenient development experience in VS Code
1. Configure profile in settings, entry: `terminal.integrated.profiles.windows`
2. Add below config as entry (choose VS 2026 or VS 2022 based on your installation):
**For Visual Studio 2026 (recommended):**
```json
"Developer PowerShell for VS": {
// Configure based on your preference
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
"args": [
"-NoExit",
"-Command",
"& {",
"$orig = Get-Location;",
// Adjust path based on your edition (Community/Professional/Enterprise)
"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
"Set-Location $orig",
"}"
]
},
```
### Configure Developer Powershell for VS 2022 or Developer Powershell for VS for more convenient dev in vscode.
1. Configure profile in in settings, entry: "terminal.integrated.profiles.windows"
2. Add below config as entry (choose VS 2022 or VS 2026 based on your installation):
**For Visual Studio 2022:**
```json
@@ -55,6 +37,24 @@ VS Code extensions Needed:
},
```
**For Visual Studio 2026:**
```json
"Developer PowerShell for VS": {
// Configure based on your preference
"path": "C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.5.2.0_arm64__8wekyb3d8bbwe\\pwsh.exe",
"args": [
"-NoExit",
"-Command",
"& {",
"$orig = Get-Location;",
// Adjust path based on your edition (Community/Professional/Enterprise)
"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\Tools\\Launch-VsDevShell.ps1';",
"Set-Location $orig",
"}"
]
},
```
3. [Optional] Set your Developer PowerShell profile as the default, so that you can get a deep integration with vscode coding agent.
4. Now you can build with plain `msbuild` or configure tasks.json in below section.

View File

@@ -12,11 +12,22 @@ A PowerToy module is a self-contained utility integrated into the PowerToys ecos
### Requirements
Follow the [Getting Started](../readme.md#getting-started) guide to set up your development environment, then [validate that you are able to build and run](debugging.md) `PowerToys.slnx`.
- [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) and the following workloads/individual components:
- Desktop Development with C++
- WinUI application development
- .NET desktop development
- Windows 10 SDK (10.0.22621.0)
- Windows 11 SDK (10.0.26100.3916)
- .NET 8 SDK
- Fork the [PowerToys repository](https://github.com/microsoft/PowerToys/tree/main) locally
- [Validate that you are able to build and run](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/development/debugging.md) `PowerToys.slnx`.
Optional:
- [WiX v5 toolset](https://github.com/microsoft/PowerToys/tree/main) for the installer
> [!NOTE]
> To ensure all the correct VS Workloads are installed, use [the WinGet configuration files](https://github.com/microsoft/PowerToys/tree/e13d6a78aafbcf32a4bb5f8581d041e1d057c3f1/.config) in the project repository. (Use the one that matches your VS distribution. ie: VS Community would use `configuration.winget`)
### Folder structure
```

View File

@@ -1,197 +0,0 @@
# Telemetry Events
PowerToys collects limited telemetry to understand feature usage, reliability, and product quality. When adding a new telemetry event, follow the steps below to ensure the event is properly declared, documented, and available after release.
**⚠️ Important**: Telemetry must never include personal information, file paths, or usergenerated content.
## Developer Effort Overview (What to Expect)
Adding a telemetry event is a **multi-step process** that typically spans several areas of the codebase and documentation.
At a high level, developers should expect to:
1. Within one PR:
1. Add a new telemetry event(s) to module
1. Add the new event(s) DATA_AND_PRIVACY.md
1. Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s)
### Privacy Guidelines
**NEVER** log:
- User data (text, files, emails, etc.)
- File paths or filenames
- Personal information
- Sensitive system information
- Anything that could identify a specific user
DO log:
- Feature usage (which features, how often)
- Success/failure status
- Timing/performance metrics
- Error types (not error messages with user data)
- Aggregate counts
### Event Naming Convention
Follow this pattern: `UtilityName_EventDescription`
Examples:
- `ColorPicker_Session`
- `FancyZones_LayoutApplied`
- `PowerRename_Rename`
- `AdvancedPaste_FormatClicked`
- `CmdPal_ExtensionInvoked`
## Adding Telemetry Events to PowerToys
PowerToys uses ETW (Event Tracing for Windows) for telemetry in both C++ and C# modules. The telemetry system is:
- Opt-in by default (disabled since v0.86)
- Privacy-focused - never logs personal info, file paths, or user-generated content
- Controlled by registry - HKEY_CURRENT_USER\Software\Classes\PowerToys\AllowDataDiagnostics
### C++ Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [ProjectTelemetry.h](../../src/common/Telemetry/ProjectTelemetry.h) | Declares the global ETW provider g_hProvider |
| [TraceBase.h](../../src/common/Telemetry/TraceBase.h) | Base class with RegisterProvider(), UnregisterProvider(), and IsDataDiagnosticsEnabled() check |
| [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Privacy tags and telemetry option group macros
#### Pattern for C++ Modules
1. Create a `Trace` class inheriting from `telemetry::TraceBase` (src/common/Telemetry/TraceBase.h):
```c
// trace.h
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
static void MyEvent(/* parameters */);
};
```
2. Implement events using `TraceLoggingWriteWrapper`:
```cpp
// trace.cpp
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::MyEvent(bool enabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"ModuleName_EventName", // Event name
TraceLoggingBoolean(enabled, "Enabled"), // Event data
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
```
**Key C++ Telemetry Macros**
| Macro | Purpose |
| ------------- |:-------------:|
| `TraceLoggingWriteWrapper` [CustomAction.cpp](../../installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp) | Wraps `TraceLoggingWrite` with `IsDataDiagnosticsEnabled()` check |
| `ProjectTelemetryPrivacyDataTag(tag)` [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Sets privacy classification |
### C# Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [PowerToysTelemetry.cs](../../src/common/ManagedTelemetry/Telemetry/PowerToysTelemetry.cs) | Singleton `Log` instance with `WriteEvent<T>()` method |
| [EventBase.cs](../../src/common/ManagedTelemetry/Telemetry/Events/EventBase.cs) | Base class for all events (provides `EventName`, `Version`) |
| [IEvent.cs](../../src/common/ManagedTelemetry/Telemetry/Events/IEvent.cs) | Interface requiring `PartA_PrivTags` property |
| [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Inherits from `EventSource`, defines ETW constants |
| [DataDiagnosticsSettings.cs](../../src/common/ManagedTelemetry/Telemetry/DataDiagnosticsSettings.cs) | Registry-based enable/disable check
#### Pattern for C# Modules
1. Create an event class inheriting from `EventBase` and implementing `IEvent`:
```csharp
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace MyModule.Telemetry
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class MyModuleEvent : EventBase, IEvent
{
// Event properties (logged as telemetry data)
public string SomeProperty { get; set; }
public int SomeValue { get; set; }
// Required: Privacy tag
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
// Optional: Set EventName in constructor (defaults to class name)
public MyModuleEvent(string prop, int val)
{
EventName = "MyModule_EventName";
SomeProperty = prop;
SomeValue = val;
}
}
}
```
2. Log the event:
```csharp
PowerToysTelemetry.Log.WriteEvent(new MyModuleEvent("value", 42));
```
**Privacy Tags (C#)**
| Tag | Use Case |
| ------------- |:-------------:|
| `PartA_PrivTags.ProductAndServiceUsage` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Feature usage events
| `PartA_PrivTags.ProductAndServicePerformance` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Performance/timing events
### Update DATA_AND_PRIVACY.md file
Add your new event(s) to [DATA_AND_PRIVACY.md](../../DATA_AND_PRIVACY.md).
## Launch Product Version Containing the new events
Events do not become active until they ship in a released PowerToys version. After your PRs are merged:
- The event will begin firing once users install the version that includes it
- In order for PowerToys to process these events, you must complete the next section
## Next Steps
Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s).
## Summary
Required steps:
1. In one PR:
- Add the event(s) in code
- Document event(s) in DATA_AND_PRIVACY.md
1. Ship the change in a PowerToys release
1. Reach out for next steps

View File

@@ -152,7 +152,7 @@ FancyZones is divided into several projects:
## Development Environment Setup
### Prerequisites
- Visual Studio 2026 (or 2022 17.4+): Required for building and debugging
- Visual Studio 2022 or 2026: Required for building and debugging
- Windows 10 SDK: Ensure the latest version is installed
- PowerToys Repository: Clone from GitHub
@@ -183,7 +183,7 @@ FancyZones is divided into several projects:
## Debugging
### Setup for Debugging
1. In Visual Studio, set FancyZonesEditor as the startup project
1. In Visual Studio 2022 or 2026, set FancyZonesEditor as the startup project
2. Set breakpoints in the code where needed
3. Click Run to start debugging

View File

@@ -2,31 +2,91 @@
Welcome to the PowerToys developer documentation. This documentation provides information for developers who want to contribute to PowerToys or understand how it works.
## Getting Started
## Core Architecture
### Prerequisites
- [Architecture Overview](core/architecture.md) - Overview of the PowerToys architecture and module interface
- [Runner and System tray](core/runner.md) - Details about the PowerToys Runner process
- [Settings](core/settings/readme.md) - Documentation on the settings system
- [Installer](core/installer.md) - Information about the PowerToys installer
- [Modules](modules/readme.md) - Documentation for individual PowerToys modules
1. Windows 10 April 2018 Update (version 1803) or newer
1. [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) (recommended) or Visual Studio 2022 17.4+ with the following workloads/components:
- Desktop Development with C++
- WinUI application development
- .NET desktop development
- Windows 10 SDK (10.0.22621.0)
- Windows 11 SDK (10.0.26100.3916)
1. .NET 8 SDK
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
## Common Components
> **Tip:** You can install Visual Studio with all required workloads automatically using the [WinGet configuration files](https://github.com/microsoft/PowerToys/tree/main/.config) in the repository:
> ```powershell
> winget configure .config\configuration.winget
> ```
> Pick the file that matches your VS edition (e.g., `configuration.vsProfessional.winget` or `configuration.vsEnterprise.winget`).
- [Context Menu Handlers](common/context-menus.md) - How PowerToys implements and registers Explorer context menu handlers
- [Monaco Editor](common/monaco-editor.md) - How PowerToys uses the Monaco code editor component across modules
- [Logging and Telemetry](development/logging.md) - How to use logging and telemetry
- [Localization](development/localization.md) - How to support multiple languages
### Fork, Clone, and Set Up
## Development Guidelines
- [Coding Guidelines](development/guidelines.md) - Development guidelines and best practices
- [Coding Style](development/style.md) - Code formatting and style conventions
- [UI Testing](development/ui-tests.md) - How to write UI tests for PowerToys
- [Debugging](development/debugging.md) - Techniques for debugging PowerToys
## Tools
- [Tools Overview](tools/readme.md) - Overview of tools in PowerToys
- [Build Tools](tools/build-tools.md) - Tools that help building PowerToys
- [Bug Report Tool](tools/bug-report-tool.md) - Tool for collecting logs and system information
- [Debugging Tools](tools/debugging-tools.md) - Specialized tools for debugging
- [Fuzzing Testing](tools/fuzzingtesting.md) - How to implement and run fuzz testing for PowerToys modules
## Processes
- [Release Process](processes/release-process.md) - How PowerToys releases are prepared and published
- [Update Process](processes/update-process.md) - How PowerToys updates work
- [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details
## Other Resources
- [aka.ms links](akaLinks.md) - List of short links
- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests
## Fork, Clone, Branch and Create your PR
Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development:
1. Fork the repo on GitHub if you haven't already
1. Clone your fork locally
1. Run the automated setup script (**recommended**):
1. Create a feature branch
1. Work on your changes
1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/)
1. When ready, mark your PR as "ready for review".
## Rules
- **Follow the pattern of what you already see in the code.**
- [Coding style](development/style.md).
- Try to package new functionality/components into libraries that have nicely defined interfaces.
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
## GitHub Workflow
- Before starting to work on a fix/feature, make sure there is an open issue to track the work.
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
- When opening a PR, follow the PR template.
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.
- Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`.
- Close issues automatically when referenced in a PR. You can use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) in the body of the PR to have GitHub automatically link your PR to the issue.
## Compiling PowerToys
### Prerequisites for Compiling PowerToys
1. Windows 10 April 2018 Update (version 1803) or newer
1. Visual Studio Community/Professional/Enterprise 2022 17.4 or newer, or Visual Studio 2026
1. A local clone of the PowerToys repository
1. Enable long paths in Windows (see [Enable Long Paths](https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation#enabling-long-paths-in-windows-10-version-1607-and-later) for details)
### Automated Setup (Recommended)
Run the setup script to automatically configure your development environment:
```powershell
.\tools\build\setup-dev-environment.ps1
@@ -38,10 +98,15 @@ This script will:
- Guide you through installing required Visual Studio components from `.vsconfig`
- Initialize git submodules
Run with `-Help` to see all available options.
Run with `-Help` to see all available options:
<details>
<summary><strong>Manual setup (if you prefer not to use the script)</strong></summary>
```powershell
.\tools\build\setup-dev-environment.ps1 -Help
```
### Manual Setup
If you prefer to set up manually, follow these steps:
#### Install Visual Studio dependencies
@@ -50,17 +115,15 @@ Run with `-Help` to see all available options.
Alternatively, import the `.vsconfig` file from the repository root using Visual Studio Installer to install all required workloads.
#### Initialize submodules
#### Get Submodules to compile
This is a one-time step required before you can compile most parts of PowerToys.
We have submodules that need to be initialized before you can compile most parts of PowerToys. This should be a one-time step.
1. Open a terminal
1. Navigate to the folder you cloned PowerToys to.
1. Run `git submodule update --init --recursive`
</details>
### Building
### Compiling Source Code
#### Using Visual Studio
@@ -88,77 +151,7 @@ You can also build from the command line using the provided scripts in `tools\bu
.\tools\build\build-installer.ps1
```
### Debugging
See [Debugging](development/debugging.md) for detailed debugging techniques, including Visual Studio setup, attaching to child processes, and troubleshooting build errors.
### Creating a New PowerToy
See [Creating a New PowerToy](development/new-powertoy.md) for an end-to-end guide covering module architecture, settings integration, installer packaging, and testing.
## Development Guidelines
- [Coding Guidelines](development/guidelines.md) - Development guidelines and best practices
- [Coding Style](development/style.md) - Code formatting and style conventions
- [Logging and Telemetry](development/logging.md) - How to use logging and telemetry
- [Localization](development/localization.md) - How to support multiple languages
- [UI Testing](development/ui-tests.md) - How to write UI tests for PowerToys
- [Developing with VS Code](development/dev-with-vscode.md) - Build, debug, and contribute using VS Code
## Rules
- **Follow the pattern of what you already see in the code.**
- [Coding style](development/style.md).
- Try to package new functionality/components into libraries that have nicely defined interfaces.
- Package new functionality into classes or refactor existing functionality into a class as you extend the code.
- When adding new classes/methods/changing existing code, add new unit tests or update the existing tests.
## GitHub Workflow
- Before starting to work on a fix/feature, make sure there is an open issue to track the work.
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
- When opening a PR, follow the PR template.
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.
- Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`.
- Close issues automatically when referenced in a PR. You can use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) in the body of the PR to have GitHub automatically link your PR to the issue.
## Core Architecture
- [Architecture Overview](core/architecture.md) - Overview of the PowerToys architecture and module interface
- [Runner and System tray](core/runner.md) - Details about the PowerToys Runner process
- [Settings](core/settings/readme.md) - Documentation on the settings system
- [Installer](core/installer.md) - Information about the PowerToys installer
- [Modules](modules/readme.md) - Documentation for individual PowerToys modules
## Common Components
- [Context Menu Handlers](common/context-menus.md) - How PowerToys implements and registers Explorer context menu handlers
- [Monaco Editor](common/monaco-editor.md) - How PowerToys uses the Monaco code editor component across modules
## Tools
- [Tools Overview](tools/readme.md) - Overview of tools in PowerToys
- [Build Tools](tools/build-tools.md) - Tools that help building PowerToys
- [Bug Report Tool](tools/bug-report-tool.md) - Tool for collecting logs and system information
- [Debugging Tools](tools/debugging-tools.md) - Specialized tools for debugging
- [Fuzzing Testing](tools/fuzzingtesting.md) - How to implement and run fuzz testing for PowerToys modules
## Processes
- [Release Process](processes/release-process.md) - How PowerToys releases are prepared and published
- [Update Process](processes/update-process.md) - How PowerToys updates work
- [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details
## Other Resources
- [aka.ms links](akaLinks.md) - List of short links
- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests
## Building the Installer
## Compile the installer
Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains the MSI and handles more complex installation logic.
- The EXE installs all prerequisites and installs PowerToys via the MSI. It has additional features such as the installation flags (see below).
@@ -172,3 +165,8 @@ The installer can only be compiled in `Release` mode; steps 1 and 2 must be perf
1. Compile `PowerToysSetup.slnx` Path from root: `installer\PowerToysSetup.slnx` (details listed below)
See [Installer](core/installer.md) for more details on building and debugging the installer.
## How to create new PowerToys
See the instructions on [how to install the PowerToys Module project template](/tools/project_template). <br />
Specifications for the [PowerToys settings API](core/settings/readme.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

BIN
doc/images/runner/tray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -88,7 +88,7 @@
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>inc;..\..\src\;..\..\src\common\Telemetry;telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
<AdditionalOptions>/await /Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
<WarningLevel>Level4</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>

View File

@@ -57,7 +57,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
const auto new_version_info = std::move(get_github_version_info_async()).get();
const auto new_version_info = get_github_version_info_async().get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
@@ -76,7 +76,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
auto downloaded_installer = download_new_version(std::get<new_version_download_info>(*new_version_info)).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetName>PowerToys.AllExperiments</TargetName>
<MockDirectory>.\Microsoft.VariantAssignment\</MockDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<!-- Experimentation is live, forcing inclusion -->
<ItemGroup Condition="'$(IsExperimentationLive)'!=''">
<!-- Newtonsoft.Json is included and a version specified in Directory.Packages.props to avoid a vulnerability from older versions. -->
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.VariantAssignment.Client" />
<PackageReference Include="Microsoft.VariantAssignment.Contract" />
<Compile Remove=".\$(MockDirectory)\Client\*.cs" />
<Compile Remove=".\$(MockDirectory)\Contract\*.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,214 @@
// 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.Globalization;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
using Microsoft.PowerToys.Telemetry;
using Microsoft.VariantAssignment.Client;
using Microsoft.VariantAssignment.Contract;
using Windows.System.Profile;
namespace AllExperiments
{
// The dependencies required to build this project are only available in the official build pipeline and are internal to Microsoft.
// However, this project is not required to build a test version of the application.
public class Experiments
{
public enum ExperimentState
{
Enabled,
Disabled,
NotLoaded,
}
#pragma warning disable SA1401 // Need to use LandingPageExperiment as a static property in OobeShellPage.xaml.cs
#pragma warning disable CA2211 // Non-constant fields should not be visible
public static ExperimentState LandingPageExperiment = ExperimentState.NotLoaded;
#pragma warning restore CA2211
#pragma warning restore SA1401
public async Task<bool> EnableLandingPageExperimentAsync()
{
if (Experiments.LandingPageExperiment != ExperimentState.NotLoaded)
{
return Experiments.LandingPageExperiment == ExperimentState.Enabled;
}
Experiments varServ = new Experiments();
await varServ.VariantAssignmentProvider_Initialize();
var landingPageExperiment = varServ.IsExperiment;
Experiments.LandingPageExperiment = landingPageExperiment ? ExperimentState.Enabled : ExperimentState.Disabled;
return landingPageExperiment;
}
private async Task VariantAssignmentProvider_Initialize()
{
IsExperiment = false;
string jsonFilePath = CreateFilePath();
var vaSettings = new VariantAssignmentClientSettings
{
Endpoint = new Uri("https://default.exp-tas.com/exptas77/a7a397e7-6fbe-4f21-a4e9-3f542e4b000e-exppowertoys/api/v1/tas"),
EnableCaching = true,
ResponseCacheTime = TimeSpan.FromMinutes(5),
};
try
{
var vaClient = vaSettings.GetTreatmentAssignmentServiceClient();
var vaRequest = GetVariantAssignmentRequest();
using var variantAssignments = await vaClient.GetVariantAssignmentsAsync(vaRequest).ConfigureAwait(false);
if (variantAssignments.AssignedVariants.Count != 0)
{
var dataVersion = variantAssignments.DataVersion;
var featureVariables = variantAssignments.GetFeatureVariables();
var assignmentContext = variantAssignments.GetAssignmentContext();
var featureFlagValue = featureVariables[0].GetStringValue();
var experimentGroup = string.Empty;
string json = File.ReadAllText(jsonFilePath);
var jsonDictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
if (jsonDictionary != null)
{
if (!jsonDictionary.TryGetValue("dataversion", out object? value))
{
value = dataVersion;
jsonDictionary.Add("dataversion", value);
}
if (!jsonDictionary.ContainsKey("variantassignment"))
{
jsonDictionary.Add("variantassignment", featureFlagValue);
}
else
{
var jsonDataVersion = value.ToString();
if (jsonDataVersion != null && int.Parse(jsonDataVersion, CultureInfo.InvariantCulture) < dataVersion)
{
jsonDictionary["dataversion"] = dataVersion;
jsonDictionary["variantassignment"] = featureFlagValue;
}
}
experimentGroup = jsonDictionary["variantassignment"].ToString();
string output = JsonSerializer.Serialize(jsonDictionary);
File.WriteAllText(jsonFilePath, output);
}
if (experimentGroup == "alternate" && AssignmentUnit != string.Empty)
{
IsExperiment = true;
}
PowerToysTelemetry.Log.WriteEvent(new OobeVariantAssignmentEvent() { AssignmentContext = assignmentContext, ClientID = AssignmentUnit });
}
}
catch (HttpRequestException ex)
{
string json = File.ReadAllText(jsonFilePath);
var jsonDictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
if (jsonDictionary != null)
{
if (jsonDictionary.TryGetValue("variantassignment", out object? value))
{
if (value.ToString() == "alternate" && AssignmentUnit != string.Empty)
{
IsExperiment = true;
}
}
else
{
jsonDictionary["variantassignment"] = "current";
}
}
string output = JsonSerializer.Serialize(jsonDictionary);
File.WriteAllText(jsonFilePath, output);
Logger.LogError("Error getting to TAS endpoint", ex);
}
catch (Exception ex)
{
Logger.LogError("Error getting variant assignments for experiment", ex);
}
}
public bool IsExperiment { get; set; }
private string? AssignmentUnit { get; set; }
private VariantAssignmentRequest GetVariantAssignmentRequest()
{
var jsonFilePath = CreateFilePath();
try
{
if (!File.Exists(jsonFilePath))
{
AssignmentUnit = Guid.NewGuid().ToString();
var data = new Dictionary<string, string>()
{
["clientid"] = AssignmentUnit,
};
string jsonData = JsonSerializer.Serialize(data);
File.WriteAllText(jsonFilePath, jsonData);
}
else
{
string json = File.ReadAllText(jsonFilePath);
var jsonDictionary = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(json);
if (jsonDictionary != null)
{
AssignmentUnit = jsonDictionary["clientid"]?.ToString();
}
}
}
catch (Exception ex)
{
Logger.LogError("Error creating/getting AssignmentUnit", ex);
}
var attrNames = new List<string> { "FlightRing", "c:InstallLanguage" };
var attrData = AnalyticsInfo.GetSystemPropertiesAsync(attrNames).AsTask().GetAwaiter().GetResult();
var flightRing = string.Empty;
var installLanguage = string.Empty;
if (attrData.ContainsKey("FlightRing"))
{
flightRing = attrData["FlightRing"];
}
if (attrData.ContainsKey("InstallLanguage"))
{
installLanguage = attrData["InstallLanguage"];
}
return new VariantAssignmentRequest
{
Parameters =
{
{ "installLanguage", installLanguage },
{ "flightRing", flightRing },
{ "clientid", AssignmentUnit },
},
};
}
private string CreateFilePath()
{
var exeDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsPath = @"Microsoft\PowerToys\experimentation.json";
var filePath = Path.Combine(exeDir, settingsPath);
return filePath;
}
}
}

View File

@@ -0,0 +1,81 @@
// 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.Diagnostics;
using System.Globalization;
using System.IO.Abstractions;
namespace AllExperiments
{
public static class Logger
{
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;
private static readonly IDirectory Directory = FileSystem.Directory;
private static readonly string ApplicationLogPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\Settings Logs\\Experimentation");
static Logger()
{
if (!Directory.Exists(ApplicationLogPath))
{
Directory.CreateDirectory(ApplicationLogPath);
}
// Using InvariantCulture since this is used for a log file name
var logFilePath = Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
}
public static void LogInfo(string message)
{
Log(message, "INFO");
}
public static void LogError(string message)
{
Log(message, "ERROR");
#if DEBUG
Debugger.Break();
#endif
}
public static void LogError(string message, Exception e)
{
Log(
message + Environment.NewLine +
e?.Message + Environment.NewLine +
"Inner exception: " + Environment.NewLine +
e?.InnerException?.Message + Environment.NewLine +
"Stack trace: " + Environment.NewLine +
e?.StackTrace,
"ERROR");
#if DEBUG
Debugger.Break();
#endif
}
private static void Log(string message, string type)
{
Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay);
Trace.Indent();
Trace.WriteLine(GetCallerInfo());
Trace.WriteLine(message);
Trace.Unindent();
}
private static string GetCallerInfo()
{
StackTrace stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(3)?.GetMethod();
var className = methodName?.DeclaringType?.Name;
return "[Method]: " + methodName?.Name + " [Class]: " + className;
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.VariantAssignment.Contract;
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Client
{
#pragma warning disable SA1200 // Using directives should be placed correctly
using TreatmentAssignmentServiceClient = VariantAssignmentServiceClient<TreatmentAssignmentServiceResponse>;
#pragma warning restore SA1200 // Using directives should be placed correctly
public static class VariantAssignmentClientExtensionMethods
{
public static IVariantAssignmentProvider GetTreatmentAssignmentServiceClient(this VariantAssignmentClientSettings settings)
{
return new TreatmentAssignmentServiceClient();
}
}
}

View File

@@ -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.VariantAssignment.Contract;
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Client
{
internal sealed partial class VariantAssignmentServiceClient<TServerResponse> : IVariantAssignmentProvider, IDisposable
where TServerResponse : VariantAssignmentServiceResponse
{
public void Dispose()
{
throw new NotImplementedException();
}
public Task<IVariantAssignmentResponse> GetVariantAssignmentsAsync(IVariantAssignmentRequest request, CancellationToken ct = default)
{
return Task.FromResult(EmptyVariantAssignmentResponse.Instance);
}
}
}

View File

@@ -0,0 +1,54 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public class EmptyVariantAssignmentResponse : IVariantAssignmentResponse
{
/// <summary>
/// Singleton instance of <see cref="EmptyVariantAssignmentResponse"/>.
/// </summary>
public static readonly IVariantAssignmentResponse Instance = new EmptyVariantAssignmentResponse();
public EmptyVariantAssignmentResponse()
{
}
public long DataVersion => 0;
public string Thumbprint => string.Empty;
/// <inheritdoc/>
public IReadOnlyCollection<IAssignedVariant> AssignedVariants => Array.Empty<IAssignedVariant>();
/// <inheritdoc/>
#pragma warning disable CS8603 // Possible null reference return.
public IFeatureVariable GetFeatureVariable(IReadOnlyList<string> path) => null;
#pragma warning restore CS8603 // Possible null reference return.
/// <inheritdoc/>
public IReadOnlyList<IFeatureVariable> GetFeatureVariables(IReadOnlyList<string> prefix) => Array.Empty<IFeatureVariable>();
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
}
string IVariantAssignmentResponse.GetAssignmentContext()
{
throw new NotImplementedException();
}
IReadOnlyList<IFeatureVariable> IVariantAssignmentResponse.GetFeatureVariables()
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,11 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public interface IAssignedVariant
{
}
}

View File

@@ -0,0 +1,16 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public interface IFeatureVariable
{
/// <summary>
/// Gets the variable's value as a string.
/// </summary>
/// <returns>String value of the variable.</returns>
string GetStringValue();
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public interface IVariantAssignmentProvider : IDisposable
{
/// <summary>
/// Computes variant assignments based on <paramref name="request"/> data.
/// </summary>
/// <param name="request">Variant assignment parameters.</param>
/// <param name="ct">Propagates notification that operations should be canceled.</param>
/// <returns>An awaitable task that returns a <see cref="IVariantAssignmentResponse"/>.</returns>
Task<IVariantAssignmentResponse> GetVariantAssignmentsAsync(IVariantAssignmentRequest request, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,15 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public interface IVariantAssignmentRequest
{
/// <summary>
/// Gets inputs used for evaluating filters, assignment units, etc.
/// </summary>
IReadOnlyCollection<(string Key, string Value)> Parameters { get; }
}
}

View File

@@ -0,0 +1,48 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
/// <summary>
/// Snapshot of variant assignments.
/// </summary>
public interface IVariantAssignmentResponse : IDisposable
{
///// <summary>
///// Gets the serial number of variant assignment configuration snapshot used when assigning variants.
///// </summary>
long DataVersion { get; }
///// <summary>
///// Get a hash of the response suitable for caching.
///// </summary>
// string Thumbprint { get; }
/// <summary>
/// Gets the variants assigned based on request parameters and a variant configuration snapshot.
/// </summary>
IReadOnlyCollection<IAssignedVariant> AssignedVariants { get; }
/// <summary>
/// Gets feature variables assigned by variants in this response.
/// </summary>
/// <param name="prefix">(Optional) Filter feature variables where <see cref="IFeatureVariable.KeySegments"/> contains the <paramref name="prefix"/>.</param>
/// <returns>Range of matching feature variables.</returns>
IReadOnlyList<IFeatureVariable> GetFeatureVariables(IReadOnlyList<string> prefix);
// this actually part of the interface but gets the job done
IReadOnlyList<IFeatureVariable> GetFeatureVariables();
// this actually part of the interface but gets the job done
string GetAssignmentContext();
/// <summary>
/// Gets a single feature variable assigned by variants in this response.
/// </summary>
/// <param name="path">Exact feature variable path.</param>
/// <returns>Matching feature variable or null.</returns>
IFeatureVariable GetFeatureVariable(IReadOnlyList<string> path);
}
}

View File

@@ -0,0 +1,11 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
internal sealed class TreatmentAssignmentServiceResponse : VariantAssignmentServiceResponse
{
}
}

View File

@@ -0,0 +1,31 @@
// 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.ComponentModel.DataAnnotations;
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
/// <summary>
/// Configuration for variant assignment service client.
/// </summary>
public class VariantAssignmentClientSettings
{
/// <summary>
/// Gets or sets the variant assignment service endpoint URL.
/// </summary>
[Required]
public Uri? Endpoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets a value whether client side request caching should be enabled.
/// </summary>
public bool EnableCaching { get; set; }
/// <summary>
/// Gets or sets the maximum time a cached variant assignment response may be used without re-validating.
/// </summary>
public TimeSpan ResponseCacheTime { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
// 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.Specialized;
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
public class VariantAssignmentRequest : IVariantAssignmentRequest
{
private NameValueCollection _parameters = new NameValueCollection();
/// <summary>
/// Gets or sets mutable <see cref="IVariantAssignmentRequest.Parameters"/>.
/// </summary>
public NameValueCollection Parameters { get => _parameters; set => _parameters = value; }
IReadOnlyCollection<(string Key, string Value)> IVariantAssignmentRequest.Parameters => (IReadOnlyCollection<(string Key, string Value)>)_parameters;
}
}

View File

@@ -0,0 +1,48 @@
// 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.
// The goal of this class is to just mock out the Microsoft.VariantAssignment close source objects
namespace Microsoft.VariantAssignment.Contract
{
/// <summary>
/// Mutable implementation of <see cref="IVariantAssignmentResponse"/> for (de)serialization.
/// </summary>
internal class VariantAssignmentServiceResponse : IVariantAssignmentResponse, IDisposable
{
/// <inheritdoc />
public virtual long DataVersion { get; set; }
public virtual IReadOnlyCollection<IAssignedVariant> AssignedVariants { get; set; } = Array.Empty<IAssignedVariant>();
public IFeatureVariable GetFeatureVariable(IReadOnlyList<string> path)
{
throw new NotImplementedException();
}
public IReadOnlyList<IFeatureVariable> GetFeatureVariables(IReadOnlyList<string> prefix)
{
throw new NotImplementedException();
}
protected virtual void Dispose(bool disposing)
{
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public IReadOnlyList<IFeatureVariable> GetFeatureVariables()
{
throw new NotImplementedException();
}
public string GetAssignmentContext()
{
return string.Empty;
}
}
}

View File

@@ -34,7 +34,8 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Check if model is in catalog
if (!EnsureModelInCatalog(modelId))
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (!isInCatalog)
{
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
Logger.LogError($"[FoundryLocal] {errorMessage}");
@@ -42,28 +43,15 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Ensure the model is loaded before returning chat client
var isLoaded = EnsureModelLoadedWithRefresh(modelId);
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
if (!isLoaded)
{
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
}
var client = _foundryClient;
if (client == null)
{
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
Logger.LogError($"[FoundryLocal] {message}");
throw new InvalidOperationException(message);
}
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
var baseUri = client.GetServiceUri();
if (baseUri == null && TryRefreshClient("Service URI was not available"))
{
baseUri = _foundryClient?.GetServiceUri();
}
var baseUri = _foundryClient.GetServiceUri();
if (baseUri == null)
{
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
@@ -136,7 +124,6 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
_serviceUrl = await _foundryClient.GetServiceUrl().ConfigureAwait(false);
return;
}
@@ -166,75 +153,4 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
Logger.LogInfo($"[FoundryLocal] Available: {available}");
return available;
}
private bool EnsureModelInCatalog(string modelId)
{
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (isInCatalog)
{
return true;
}
Logger.LogWarning($"[FoundryLocal] Model not found in catalog. Refreshing client for model: {modelId}");
if (!TryRefreshClient("Model not in catalog"))
{
return false;
}
return _catalogModels?.Any(m => m.Name == modelId) ?? false;
}
private bool EnsureModelLoadedWithRefresh(string modelId)
{
var isLoaded = false;
try
{
isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogWarning($"[FoundryLocal] EnsureModelLoaded failed: {ex.Message}");
}
if (isLoaded)
{
return true;
}
if (!TryRefreshClient("EnsureModelLoaded failed"))
{
return false;
}
try
{
return _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] EnsureModelLoaded failed after refresh: {ex.Message}", ex);
return false;
}
}
private bool TryRefreshClient(string reason)
{
Logger.LogInfo($"[FoundryLocal] Refreshing Foundry Local client: {reason}");
try
{
_foundryClient = null;
_catalogModels = null;
_serviceUrl = null;
InitializeAsync().GetAwaiter().GetResult();
return _foundryClient != null;
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] Failed to refresh Foundry Local client: {ex.Message}", ex);
return false;
}
}
}

View File

@@ -41,7 +41,6 @@ namespace ManagedCommon
/// black. Calls <c>DwmExtendFrameIntoClientArea()</c> with a <c>cyTopHeight</c> of 2 to force
/// the window's top border to be visible.<br/><br/>
/// Is a no-op on versions other than Windows 10.
/// WinUI issue: https://github.com/microsoft/microsoft-ui-xaml/issues/6901
/// </summary>
public static void ForceTopBorder1PixelInsetOnWindows10(IntPtr handle)
{

View File

@@ -18,7 +18,7 @@ namespace // Strings in this namespace should not be localized
namespace updating
{
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async()
std::future<bool> uninstall_previous_msix_version_async()
{
winrt::Windows::Management::Deployment::PackageManager package_manager;

View File

@@ -2,11 +2,11 @@
#include <string>
#include <optional>
#include <future>
#include <winrt/Windows.Foundation.h>
#include <common/version/helper.h>
namespace updating
{
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async();
}
std::future<bool> uninstall_previous_msix_version_async();
}

View File

@@ -33,7 +33,6 @@
#include <winrt/Windows.System.h>
#include <wil/resource.h>
#include <wil/coroutine.h>
#endif //PCH_H

View File

@@ -82,7 +82,11 @@ namespace updating
// prevent the warning that may show up depend on the value of the constants (#defines)
#pragma warning(push)
#pragma warning(disable : 4702)
wil::task<github_version_result> get_github_version_info_async(const bool prerelease)
#if USE_STD_EXPECTED
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
#else
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
#endif
{
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
if constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
@@ -166,7 +170,7 @@ namespace updating
return !ec ? std::optional{ installer_download_path } : std::nullopt;
}
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version)
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version)
{
auto installer_download_path = create_download_path();
if (!installer_download_path)

View File

@@ -2,6 +2,7 @@
#include <optional>
#include <string>
#include <future>
#include <filesystem>
#include <variant>
#include <winrt/Windows.Foundation.h>
@@ -15,7 +16,6 @@
#endif
#include <common/version/helper.h>
#include <wil/coroutine.h>
namespace updating
{
@@ -32,15 +32,13 @@ namespace updating
};
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
#if USE_STD_EXPECTED
using github_version_result = std::expected<github_version_info, std::wstring>;
#else
using github_version_result = nonstd::expected<github_version_info, std::wstring>;
#endif
wil::task<github_version_result> get_github_version_info_async(bool prerelease = false);
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version);
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version);
std::filesystem::path get_pending_updates_path();
#if USE_STD_EXPECTED
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
#else
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
#endif
void cleanup_updates();
// non-localized

View File

@@ -1,7 +1,6 @@
#pragma once
#include <functional>
#include <string>
#include <future>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Web.Http.h>
@@ -22,15 +21,15 @@ namespace http
headers.UserAgent().TryParseAdd(USER_AGENT);
}
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> request(winrt::Windows::Foundation::Uri url)
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
auto body = co_await response.Content().ReadAsStringAsync();
co_return body;
co_return std::wstring(body);
}
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath)
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
@@ -39,7 +38,7 @@ namespace http
file_stream.Close();
}
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath, std::function<void(float)> progressUpdateCallback)
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
{
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
response.EnsureSuccessStatusCode();

View File

@@ -13,6 +13,7 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -13,6 +13,7 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -13,6 +13,7 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -5140,7 +5140,7 @@ bool IsPenInverted( WPARAM wParam )
// Captures the specified screen using the capture APIs
//
//----------------------------------------------------------------------------
wil::task<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
{
auto d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(device);
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
@@ -5176,7 +5176,9 @@ wil::task<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect
framePool.Close();
auto texture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
co_return util::CopyD3DTexture(d3dDevice, texture, true);
auto result = util::CopyD3DTexture(d3dDevice, texture, true);
co_return result;
}
//----------------------------------------------------------------------------
@@ -5203,7 +5205,10 @@ winrt::com_ptr<ID3D11Texture2D>CaptureScreenshot(winrt::DirectXPixelFormat const
auto item = util::CreateCaptureItemForMonitor(hMon);
return CaptureScreenshotAsync(device, item, pixelFormat).get();
auto capture = CaptureScreenshotAsync(device, item, pixelFormat);
capture.wait();
return capture.get();
}

View File

@@ -69,7 +69,6 @@
// WIL
#include <wil/com.h>
#include <wil/resource.h>
#include <wil/coroutine.h>
// DirectX
#include <d3d11_4.h>

View File

@@ -1,26 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common;
/// <summary>
/// Represents the packaging flavor of the application.
/// </summary>
public enum AppPackagingFlavor
{
/// <summary>
/// Application is packaged as a Windows MSIX package.
/// </summary>
Packaged,
/// <summary>
/// Application is running unpackaged (native executable).
/// </summary>
Unpackaged,
/// <summary>
/// Application is running as unpackaged portable (self-contained distribution).
/// </summary>
UnpackagedPortable,
}

View File

@@ -1,27 +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.Core.Common.Text;
namespace Microsoft.CmdPal.Core.Common.Helpers;
/// <summary>
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
/// </summary>
public interface IPrecomputedListItem
{
/// <summary>
/// Gets the fuzzy matching target for the item's title.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the title.</returns>
FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher);
/// <summary>
/// Gets the fuzzy matching target for the item's subtitle.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the subtitle.</returns>
FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher);
}

View File

@@ -1,142 +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.Buffers;
using System.Diagnostics;
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Core.Common.Helpers;
public static partial class InternalListHelpers
{
public static RoScored<T>[] FilterListWithScores<T>(
IEnumerable<T>? items,
in FuzzyQuery query,
in ScoringFunction<T> scoreFunction)
{
if (items == null)
{
return [];
}
// Try to get initial capacity hint
var initialCapacity = items switch
{
ICollection<T> col => col.Count,
IReadOnlyCollection<T> rc => rc.Count,
_ => 64,
};
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
var count = 0;
try
{
foreach (var item in items)
{
var score = scoreFunction(in query, item);
if (score <= 0)
{
continue;
}
if (count == buffer.Length)
{
GrowBuffer(ref buffer, count);
}
buffer[count++] = new RoScored<T>(item, score);
}
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
var result = GC.AllocateUninitializedArray<RoScored<T>>(count);
buffer.AsSpan(0, count).CopyTo(result);
return result;
}
finally
{
ArrayPool<RoScored<T>>.Shared.Return(buffer);
}
}
private static void GrowBuffer<T>(ref RoScored<T>[] buffer, int count)
{
var newBuffer = ArrayPool<RoScored<T>>.Shared.Rent(buffer.Length * 2);
buffer.AsSpan(0, count).CopyTo(newBuffer);
ArrayPool<RoScored<T>>.Shared.Return(buffer);
buffer = newBuffer;
}
public static T[] FilterList<T>(IEnumerable<T> items, in FuzzyQuery query, ScoringFunction<T> scoreFunction)
{
// Try to get initial capacity hint
var initialCapacity = items switch
{
ICollection<T> col => col.Count,
IReadOnlyCollection<T> rc => rc.Count,
_ => 64,
};
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
var count = 0;
try
{
foreach (var item in items)
{
var score = scoreFunction(in query, item);
if (score <= 0)
{
continue;
}
if (count == buffer.Length)
{
GrowBuffer(ref buffer, count);
}
buffer[count++] = new RoScored<T>(item, score);
}
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
var result = GC.AllocateUninitializedArray<T>(count);
for (var i = 0; i < count; i++)
{
result[i] = buffer[i].Item;
}
return result;
}
finally
{
ArrayPool<RoScored<T>>.Shared.Return(buffer);
}
}
private readonly struct RoScoredDescendingComparer<T> : IComparer<RoScored<T>>
{
public int Compare(RoScored<T> x, RoScored<T> y) => y.Score.CompareTo(x.Score);
}
}
public delegate int ScoringFunction<in T>(in FuzzyQuery query, T item);
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public readonly struct RoScored<T>
{
public readonly int Score;
public readonly T Item;
public RoScored(T item, int score)
{
Score = score;
Item = item;
}
private string GetDebuggerDisplay()
{
return "Score = " + Score + ", Item = " + Item;
}
}

View File

@@ -1,87 +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.Diagnostics;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Core.Common.Helpers;
/// <summary>
/// Helper class for retrieving application version information safely.
/// </summary>
internal static class VersionHelper
{
/// <summary>
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
/// Falls back to assembly version if packaged version is unavailable, and returns a default value if both fail.
/// </summary>
/// <returns>The application version string, or a fallback value if retrieval fails.</returns>
public static string GetAppVersionSafe()
{
if (TryGetPackagedVersion(out var version))
{
return version;
}
if (TryGetAssemblyVersion(out version))
{
return version;
}
return "?";
}
/// <summary>
/// Attempts to retrieve the application version from the package manifest.
/// </summary>
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
private static bool TryGetPackagedVersion(out string version)
{
version = string.Empty;
try
{
// Package.Current throws InvalidOperationException if the app is not packaged
var v = Package.Current.Id.Version;
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
return true;
}
catch (InvalidOperationException)
{
return false;
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to get version from the package", ex);
return false;
}
}
/// <summary>
/// Attempts to retrieve the application version from the executable file.
/// </summary>
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
private static bool TryGetAssemblyVersion(out string version)
{
version = string.Empty;
try
{
var processPath = Environment.ProcessPath;
if (string.IsNullOrEmpty(processPath))
{
return false;
}
var info = FileVersionInfo.GetVersionInfo(processPath);
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
return true;
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to get version from the executable", ex);
return false;
}
}
}

View File

@@ -1,125 +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.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Core.Common.Services;
/// <summary>
/// Implementation of IApplicationInfoService providing application-wide information.
/// </summary>
public sealed class ApplicationInfoService : IApplicationInfoService
{
private readonly Lazy<string> _configDirectory = new(() => Utilities.BaseSettingsPath("Microsoft.CmdPal"));
private readonly Lazy<bool> _isElevated;
private readonly Lazy<string> _logDirectory;
private readonly Lazy<AppPackagingFlavor> _packagingFlavor;
private Func<string>? _getLogDirectory;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class.
/// The log directory delegate can be set later via <see cref="SetLogDirectory(Func{string})"/>.
/// </summary>
public ApplicationInfoService()
{
_packagingFlavor = new Lazy<AppPackagingFlavor>(DeterminePackagingFlavor);
_isElevated = new Lazy<bool>(DetermineElevationStatus);
_logDirectory = new Lazy<string>(() => _getLogDirectory?.Invoke() ?? "Not available");
}
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class with an optional log directory provider.
/// </summary>
/// <param name="getLogDirectory">Optional delegate to retrieve the log directory path. If not provided, the log directory will be unavailable.</param>
public ApplicationInfoService(Func<string>? getLogDirectory)
: this()
{
_getLogDirectory = getLogDirectory;
}
/// <summary>
/// Sets the log directory delegate to be used for retrieving the log directory path.
/// This allows deferred initialization of the logger path.
/// </summary>
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
public void SetLogDirectory(Func<string> getLogDirectory)
{
ArgumentNullException.ThrowIfNull(getLogDirectory);
_getLogDirectory = getLogDirectory;
}
public string AppVersion => VersionHelper.GetAppVersionSafe();
public AppPackagingFlavor PackagingFlavor => _packagingFlavor.Value;
public string LogDirectory => _logDirectory.Value;
public string ConfigDirectory => _configDirectory.Value;
public bool IsElevated => _isElevated.Value;
public string GetApplicationInfoSummary()
{
return $"""
Application:
App version: {AppVersion}
Packaging flavor: {PackagingFlavor}
Is elevated: {(IsElevated ? "yes" : "no")}
Environment:
OS version: {RuntimeInformation.OSDescription}
OS architecture: {RuntimeInformation.OSArchitecture}
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
Framework: {RuntimeInformation.FrameworkDescription}
Process architecture: {RuntimeInformation.ProcessArchitecture}
Culture: {CultureInfo.CurrentCulture.Name}
UI culture: {CultureInfo.CurrentUICulture.Name}
Paths:
Log directory: {LogDirectory}
Config directory: {ConfigDirectory}
""";
}
private static AppPackagingFlavor DeterminePackagingFlavor()
{
// Try to determine if running as packaged
try
{
// If this doesn't throw, we're packaged
_ = Package.Current.Id.Version;
return AppPackagingFlavor.Packaged;
}
catch (InvalidOperationException)
{
// Not packaged, check if portable
// For now, we don't support portable yet, so return Unpackaged
// In the future, check for a marker file or environment variable
return AppPackagingFlavor.Unpackaged;
}
catch (Exception ex)
{
CoreLogger.LogError("Failed to determine packaging flavor", ex);
return AppPackagingFlavor.Unpackaged;
}
}
private static bool DetermineElevationStatus()
{
try
{
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
return isElevated;
}
catch (Exception)
{
return false;
}
}
}

View File

@@ -1,49 +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.Core.Common.Services;
/// <summary>
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
/// </summary>
public interface IApplicationInfoService
{
/// <summary>
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
/// </summary>
string AppVersion { get; }
/// <summary>
/// Gets the packaging flavor of the application.
/// </summary>
AppPackagingFlavor PackagingFlavor { get; }
/// <summary>
/// Gets the directory path where application logs are stored.
/// </summary>
string LogDirectory { get; }
/// <summary>
/// Gets the directory path where application configuration files are stored.
/// </summary>
string ConfigDirectory { get; }
/// <summary>
/// Gets a value indicating whether the application is running with administrator privileges.
/// </summary>
bool IsElevated { get; }
/// <summary>
/// Gets a formatted summary of application information suitable for logging.
/// </summary>
/// <returns>A formatted string containing application information.</returns>
string GetApplicationInfoSummary();
/// <summary>
/// Sets the log directory delegate to be used for retrieving the log directory path.
/// This allows deferred initialization of the logger path.
/// </summary>
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
void SetLogDirectory(Func<string> getLogDirectory);
}

View File

@@ -2,27 +2,20 @@
// 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.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
public sealed class ErrorReportBuilder : IErrorReportBuilder
{
private readonly ErrorReportSanitizer _sanitizer = new();
private readonly IApplicationInfoService _appInfoService;
private static string Preamble => Properties.Resources.ErrorReport_Global_Preamble;
/// <summary>
/// Initializes a new instance of the <see cref="ErrorReportBuilder"/> class.
/// </summary>
/// <param name="appInfoService">Optional application info service. If not provided, a default instance is created.</param>
public ErrorReportBuilder(IApplicationInfoService? appInfoService = null)
{
_appInfoService = appInfoService ?? new ApplicationInfoService(null);
}
public string BuildReport(Exception exception, string context, bool redactPii = true)
{
ArgumentNullException.ThrowIfNull(exception);
@@ -31,9 +24,6 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
var sanitizedMessage = redactPii ? _sanitizer.Sanitize(exceptionMessage) : exceptionMessage;
var sanitizedFormattedException = redactPii ? _sanitizer.Sanitize(exception.ToString()) : exception.ToString();
var applicationInfoSummary = GetAppInfoSafe();
var applicationInfoSummarySanitized = redactPii ? _sanitizer.Sanitize(applicationInfoSummary) : applicationInfoSummary;
// Note:
// - do not localize technical part of the report, we need to ensure it can be read by developers
// - keep timestamp format should be consistent with the log (makes it easier to search)
@@ -48,7 +38,18 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
Context: {context ?? "N/A"}
{applicationInfoSummarySanitized}
Application:
App version: {GetAppVersionSafe()}
Is elevated: {GetElevationStatus()}
Environment:
OS version: {RuntimeInformation.OSDescription}
OS architecture: {RuntimeInformation.OSArchitecture}
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
Framework: {RuntimeInformation.FrameworkDescription}
Process architecture: {RuntimeInformation.ProcessArchitecture}
Culture: {CultureInfo.CurrentCulture.Name}
UI culture: {CultureInfo.CurrentUICulture.Name}
Stack Trace:
{exception.StackTrace}
@@ -65,17 +66,31 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
""";
}
private string? GetAppInfoSafe()
private static string GetElevationStatus()
{
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
try
{
return _appInfoService.GetApplicationInfoSummary();
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
return isElevated ? "yes" : "no";
}
catch (Exception ex)
catch (Exception)
{
// Getting application info should never throw, but if it does, we don't want it to prevent the report from being generated
var message = CoalesceExceptionMessage(ex);
return $"Failed to get application info summary: {message}";
return "Failed to determine elevation status";
}
}
private static string GetAppVersionSafe()
{
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
try
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
catch (Exception)
{
return "Failed to retrieve app version";
}
}

View File

@@ -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.Runtime.CompilerServices;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class BloomFilter : IBloomFilter
{
public ulong Compute(string input)
{
ulong bloom = 0;
foreach (var ch in input)
{
if (SymbolClassifier.Classify(ch) == SymbolKind.WordSeparator)
{
continue;
}
var h = (uint)ch * 0x45d9f3b;
bloom |= 1UL << (int)(h & 31);
bloom |= 1UL << (int)(((h >> 16) & 31) + 32);
if (bloom == ulong.MaxValue)
{
break;
}
}
return bloom;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MightContain(ulong candidateBloom, ulong queryBloom)
{
return (candidateBloom & queryBloom) == queryBloom;
}
}

View File

@@ -1,52 +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.Globalization;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class FuzzyMatcherProvider : IFuzzyMatcherProvider
{
private readonly IBloomFilter _bloomCalculator = new BloomFilter();
private readonly IStringFolder _normalizer = new StringFolder();
private IPrecomputedFuzzyMatcher _current;
public FuzzyMatcherProvider(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
_current = CreateMatcher(core, pinyin);
}
public IPrecomputedFuzzyMatcher Current => Volatile.Read(ref _current);
public void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
Volatile.Write(ref _current, CreateMatcher(core, pinyin));
}
private IPrecomputedFuzzyMatcher CreateMatcher(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin)
{
return pinyin is null || !IsPinyinEnabled(pinyin)
? new PrecomputedFuzzyMatcher(core, _normalizer, _bloomCalculator)
: new PrecomputedFuzzyMatcherWithPinyin(core, pinyin, _normalizer, _bloomCalculator);
}
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o)
{
return o.Mode switch
{
PinyinMode.Off => false,
PinyinMode.On => true,
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
_ => false,
};
}
private static bool IsSimplifiedChineseUi()
{
var culture = CultureInfo.CurrentUICulture;
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,65 +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.Core.Common.Text;
public readonly struct FuzzyQuery
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly int EffectiveLength;
public readonly bool IsAllLowercaseAsciiOrNonLetter;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public readonly int SecondaryEffectiveLength;
public readonly bool SecondaryIsAllLowercaseAsciiOrNonLetter;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyQuery(
string original,
string folded,
ulong bloom,
int effectiveLength,
bool isAllLowercaseAsciiOrNonLetter,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0,
int secondaryEffectiveLength = 0,
bool secondaryIsAllLowercaseAsciiOrNonLetter = true)
{
Original = original;
Folded = folded;
Bloom = bloom;
EffectiveLength = effectiveLength;
IsAllLowercaseAsciiOrNonLetter = isAllLowercaseAsciiOrNonLetter;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
SecondaryEffectiveLength = secondaryEffectiveLength;
SecondaryIsAllLowercaseAsciiOrNonLetter = secondaryIsAllLowercaseAsciiOrNonLetter;
}
}

View File

@@ -1,46 +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.Core.Common.Text;
public readonly struct FuzzyTarget
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public int SecondaryLength => SecondaryFolded?.Length ?? 0;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyTarget(
string original,
string folded,
ulong bloom,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0)
{
Original = original;
Folded = folded;
Bloom = bloom;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
}
}

View File

@@ -1,34 +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.Core.Common.Text;
public struct FuzzyTargetCache
{
private string? _lastRaw;
private uint _schemaId;
private FuzzyTarget _target;
public FuzzyTarget GetOrUpdate(IPrecomputedFuzzyMatcher matcher, string? raw)
{
raw ??= string.Empty;
if (_schemaId == matcher.SchemaId && string.Equals(_lastRaw, raw, StringComparison.Ordinal))
{
return _target;
}
_target = matcher.PrecomputeTarget(raw);
_schemaId = matcher.SchemaId;
_lastRaw = raw;
return _target;
}
public void Invalidate()
{
_lastRaw = null;
_target = default;
_schemaId = 0;
}
}

View File

@@ -1,12 +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.Core.Common.Text;
public interface IBloomFilter
{
ulong Compute(string input);
bool MightContain(ulong candidateBloom, ulong queryBloom);
}

View File

@@ -1,12 +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.Core.Common.Text;
public interface IFuzzyMatcherProvider
{
IPrecomputedFuzzyMatcher Current { get; }
void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null);
}

View File

@@ -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.
namespace Microsoft.CmdPal.Core.Common.Text;
public interface IPrecomputedFuzzyMatcher
{
uint SchemaId { get; }
FuzzyQuery PrecomputeQuery(string? input);
FuzzyTarget PrecomputeTarget(string? input);
int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target);
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public interface IStringFolder
{
string Fold(string input, bool removeDiacritics);
}

View File

@@ -1,13 +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.Core.Common.Text;
public sealed class PinyinFuzzyMatcherOptions
{
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
public bool RemoveApostrophesForQuery { get; init; } = true;
}

View File

@@ -1,12 +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.Core.Common.Text;
public enum PinyinMode
{
Off = 0,
AutoSimplifiedChineseUi = 1,
On = 2,
}

View File

@@ -1,575 +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.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcher : IPrecomputedFuzzyMatcher
{
private const int NoMatchScore = 0;
private const int StackallocThresholdChars = 512;
private const int FolderSchemaVersion = 1;
private const int BloomSchemaVersion = 1;
private readonly PrecomputedFuzzyMatcherOptions _options;
private readonly IStringFolder _stringFolder;
private readonly IBloomFilter _bloom;
public PrecomputedFuzzyMatcher(
PrecomputedFuzzyMatcherOptions? options = null,
IStringFolder? normalization = null,
IBloomFilter? bloomCalculator = null)
{
_options = options ?? PrecomputedFuzzyMatcherOptions.Default;
_bloom = bloomCalculator ?? new BloomFilter();
_stringFolder = normalization ?? new StringFolder();
SchemaId = ComputeSchemaId(_options);
}
public uint SchemaId { get; }
public FuzzyQuery PrecomputeQuery(string? input) => PrecomputeQuery(input, null);
public FuzzyTarget PrecomputeTarget(string? input) => PrecomputeTarget(input, null);
public int Score(in FuzzyQuery query, in FuzzyTarget target)
{
var qFold = query.FoldedSpan;
var tLen = target.Length;
if (query.EffectiveLength == 0 || tLen == 0)
{
return NoMatchScore;
}
var skipWordSeparators = _options.SkipWordSeparators;
var bestScore = 0;
// 1. Primary → Primary
if (tLen >= query.EffectiveLength && _bloom.MightContain(target.Bloom, query.Bloom))
{
if (CanMatchSubsequence(qFold, target.FoldedSpan, skipWordSeparators))
{
bestScore = ScoreNonContiguous(
qRaw: query.OriginalSpan,
qFold: qFold,
qEffectiveLen: query.EffectiveLength,
tRaw: target.OriginalSpan,
tFold: target.FoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
}
}
// 2. Secondary → Secondary
if (query.HasSecondary && target.HasSecondary)
{
var qSecFold = query.SecondaryFoldedSpan;
if (target.SecondaryLength >= query.SecondaryEffectiveLength &&
_bloom.MightContain(target.SecondaryBloom, query.SecondaryBloom) &&
CanMatchSubsequence(qSecFold, target.SecondaryFoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.SecondaryOriginalSpan,
qFold: qSecFold,
qEffectiveLen: query.SecondaryEffectiveLength,
tRaw: target.SecondaryOriginalSpan,
tFold: target.SecondaryFoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
// 3. Primary query → Secondary target
if (target.HasSecondary &&
target.SecondaryLength >= query.EffectiveLength &&
_bloom.MightContain(target.SecondaryBloom, query.Bloom))
{
if (CanMatchSubsequence(qFold, target.SecondaryFoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.OriginalSpan,
qFold: qFold,
qEffectiveLen: query.EffectiveLength,
tRaw: target.SecondaryOriginalSpan,
tFold: target.SecondaryFoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
// 4. Secondary query → Primary target
if (query.HasSecondary &&
tLen >= query.SecondaryEffectiveLength &&
_bloom.MightContain(target.Bloom, query.SecondaryBloom))
{
var qSecFold = query.SecondaryFoldedSpan;
if (CanMatchSubsequence(qSecFold, target.FoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.SecondaryOriginalSpan,
qFold: qSecFold,
qEffectiveLen: query.SecondaryEffectiveLength,
tRaw: target.OriginalSpan,
tFold: target.FoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
return bestScore;
}
private FuzzyQuery PrecomputeQuery(string? input, string? secondaryInput)
{
input ??= string.Empty;
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
var bloom = _bloom.Compute(folded);
var effectiveLength = _options.SkipWordSeparators
? folded.Length - CountWordSeparators(folded)
: folded.Length;
var isAllLowercase = IsAllLowercaseAsciiOrNonLetter(input);
string? secondaryOriginal = null;
string? secondaryFolded = null;
ulong secondaryBloom = 0;
var secondaryEffectiveLength = 0;
var secondaryIsAllLowercase = true;
if (!string.IsNullOrEmpty(secondaryInput))
{
secondaryOriginal = secondaryInput;
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
secondaryBloom = _bloom.Compute(secondaryFolded);
secondaryEffectiveLength = _options.SkipWordSeparators
? secondaryFolded.Length - CountWordSeparators(secondaryFolded)
: secondaryFolded.Length;
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondaryInput);
}
return new FuzzyQuery(
original: input,
folded: folded,
bloom: bloom,
effectiveLength: effectiveLength,
isAllLowercaseAsciiOrNonLetter: isAllLowercase,
secondaryOriginal: secondaryOriginal,
secondaryFolded: secondaryFolded,
secondaryBloom: secondaryBloom,
secondaryEffectiveLength: secondaryEffectiveLength,
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int CountWordSeparators(string s)
{
var count = 0;
foreach (var c in s)
{
if (SymbolClassifier.Classify(c) == SymbolKind.WordSeparator)
{
count++;
}
}
return count;
}
}
internal FuzzyTarget PrecomputeTarget(string? input, string? secondaryInput)
{
input ??= string.Empty;
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
var bloom = _bloom.Compute(folded);
string? secondaryFolded = null;
ulong secondaryBloom = 0;
if (!string.IsNullOrEmpty(secondaryInput))
{
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
secondaryBloom = _bloom.Compute(secondaryFolded);
}
return new FuzzyTarget(
input,
folded,
bloom,
secondaryInput,
secondaryFolded,
secondaryBloom);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
{
foreach (var c in s)
{
if ((uint)(c - 'A') <= ('Z' - 'A'))
{
return false;
}
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanMatchSubsequence(
ReadOnlySpan<char> qFold,
ReadOnlySpan<char> tFold,
bool skipWordSeparators)
{
var qi = 0;
var ti = 0;
while (qi < qFold.Length && ti < tFold.Length)
{
var qChar = qFold[qi];
if (skipWordSeparators && SymbolClassifier.Classify(qChar) == SymbolKind.WordSeparator)
{
qi++;
continue;
}
if (qChar == tFold[ti])
{
qi++;
}
ti++;
}
// Skip trailing word separators in query
if (skipWordSeparators)
{
while (qi < qFold.Length && SymbolClassifier.Classify(qFold[qi]) == SymbolKind.WordSeparator)
{
qi++;
}
}
return qi == qFold.Length;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
[SkipLocalsInit]
private int ScoreNonContiguous(
scoped in ReadOnlySpan<char> qRaw,
scoped in ReadOnlySpan<char> qFold,
int qEffectiveLen,
scoped in ReadOnlySpan<char> tRaw,
scoped in ReadOnlySpan<char> tFold,
bool ignoreSameCaseBonusForThisQuery)
{
Debug.Assert(qRaw.Length == qFold.Length, "Original and folded spans are traversed in lockstep: requires qRaw.Length == qFold.Length");
Debug.Assert(tRaw.Length == tFold.Length, "Original and folded spans are traversed in lockstep: requires tRaw.Length == tFold.Length");
Debug.Assert(qEffectiveLen <= qFold.Length, "Effective length must be less than or equal to folded length");
var qLen = qFold.Length;
var tLen = tFold.Length;
// Copy options to local variables to avoid repeated field accesses
var charMatchBonus = _options.CharMatchBonus;
var sameCaseBonus = ignoreSameCaseBonusForThisQuery ? 0 : _options.SameCaseBonus;
var consecutiveMultiplier = _options.ConsecutiveMultiplier;
var camelCaseBonus = _options.CamelCaseBonus;
var startOfWordBonus = _options.StartOfWordBonus;
var pathSeparatorBonus = _options.PathSeparatorBonus;
var wordSeparatorBonus = _options.WordSeparatorBonus;
var separatorAlignmentBonus = _options.SeparatorAlignmentBonus;
var exactSeparatorBonus = _options.ExactSeparatorBonus;
var skipWordSeparators = _options.SkipWordSeparators;
// DP buffer: two rows of length tLen
var bufferSize = tLen * 2;
int[]? rented = null;
try
{
scoped Span<int> buffer;
if (bufferSize <= StackallocThresholdChars)
{
buffer = stackalloc int[bufferSize];
}
else
{
rented = ArrayPool<int>.Shared.Rent(bufferSize);
buffer = rented.AsSpan(0, bufferSize);
}
var scores = buffer[..tLen];
var seqLens = buffer.Slice(tLen, tLen);
scores.Clear();
seqLens.Clear();
ref var scores0 = ref MemoryMarshal.GetReference(scores);
ref var seqLens0 = ref MemoryMarshal.GetReference(seqLens);
ref var qRaw0 = ref MemoryMarshal.GetReference(qRaw);
ref var qFold0 = ref MemoryMarshal.GetReference(qFold);
ref var tRaw0 = ref MemoryMarshal.GetReference(tRaw);
ref var tFold0 = ref MemoryMarshal.GetReference(tFold);
var qiEffective = 0;
for (var qi = 0; qi < qLen; qi++)
{
var qCharFold = Unsafe.Add(ref qFold0, qi);
var qCharKind = SymbolClassifier.Classify(qCharFold);
if (skipWordSeparators && qCharKind == SymbolKind.WordSeparator)
{
continue;
}
// Hoisted values
var qRawIsUpper = char.IsUpper(Unsafe.Add(ref qRaw0, qi));
// row computation
var leftScore = 0;
var diagScore = 0;
var diagSeqLen = 0;
// limit ti to ensure enough remaining characters to match the rest of the query
var tiMax = tLen - qEffectiveLen + qiEffective;
for (var ti = 0; ti <= tiMax; ti++)
{
var upScore = Unsafe.Add(ref scores0, ti);
var upSeqLen = Unsafe.Add(ref seqLens0, ti);
var charScore = 0;
if (diagScore != 0 || qiEffective == 0)
{
charScore = ComputeCharScore(
qi,
ti,
qCharFold,
qCharKind,
diagSeqLen,
qRawIsUpper,
ref tRaw0,
ref qFold0,
ref tFold0);
}
var candidateScore = diagScore + charScore;
if (charScore != 0 && candidateScore >= leftScore)
{
Unsafe.Add(ref scores0, ti) = candidateScore;
Unsafe.Add(ref seqLens0, ti) = diagSeqLen + 1;
leftScore = candidateScore;
}
else
{
Unsafe.Add(ref scores0, ti) = leftScore;
Unsafe.Add(ref seqLens0, ti) = 0;
/* leftScore remains unchanged */
}
diagScore = upScore;
diagSeqLen = upSeqLen;
}
// Early exit: no match possible
if (leftScore == 0)
{
return NoMatchScore;
}
// Advance effective query index
// Only counts non-separator characters if skipWordSeparators is enabled
qiEffective++;
if (qiEffective == qEffectiveLen)
{
return leftScore;
}
}
return scores[tLen - 1];
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
int ComputeCharScore(
int qi,
int ti,
char qCharFold,
SymbolKind qCharKind,
int seqLen,
bool qCharRawCurrIsUpper,
ref char tRaw0,
ref char qFold0,
ref char tFold0)
{
// Match check:
// - exact folded char match always ok
// - otherwise, allow equivalence only for word separators (e.g. '_' matches '-')
var tCharFold = Unsafe.Add(ref tFold0, ti);
if (qCharFold != tCharFold)
{
if (!skipWordSeparators)
{
return 0;
}
if (qCharKind != SymbolKind.WordSeparator ||
SymbolClassifier.Classify(tCharFold) != SymbolKind.WordSeparator)
{
return 0;
}
}
// 0. Base char match bonus
var score = charMatchBonus;
// 1. Consecutive match bonus
if (seqLen > 0)
{
score += seqLen * consecutiveMultiplier;
}
// 2. Same case bonus
// Early outs to appease the branch predictor
if (sameCaseBonus != 0)
{
var tCharRawCurr = Unsafe.Add(ref tRaw0, ti);
var tCharRawCurrIsUpper = char.IsUpper(tCharRawCurr);
if (qCharRawCurrIsUpper == tCharRawCurrIsUpper)
{
score += sameCaseBonus;
}
if (ti == 0)
{
score += startOfWordBonus;
return score;
}
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
if (tPrevKind != SymbolKind.Other)
{
score += tPrevKind == SymbolKind.PathSeparator
? pathSeparatorBonus
: wordSeparatorBonus;
if (skipWordSeparators && seqLen == 0 && qi > 0)
{
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
if (qPrevKind == SymbolKind.WordSeparator)
{
score += separatorAlignmentBonus;
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
{
score += exactSeparatorBonus;
}
}
}
return score;
}
if (tCharRawCurrIsUpper && seqLen == 0)
{
score += camelCaseBonus;
return score;
}
return score;
}
else
{
if (ti == 0)
{
score += startOfWordBonus;
return score;
}
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
if (tPrevKind != SymbolKind.Other)
{
score += tPrevKind == SymbolKind.PathSeparator
? pathSeparatorBonus
: wordSeparatorBonus;
if (skipWordSeparators && seqLen == 0 && qi > 0)
{
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
if (qPrevKind == SymbolKind.WordSeparator)
{
score += separatorAlignmentBonus;
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
{
score += exactSeparatorBonus;
}
}
}
return score;
}
if (camelCaseBonus != 0 && seqLen == 0 && char.IsUpper(Unsafe.Add(ref tRaw0, ti)))
{
score += camelCaseBonus;
return score;
}
return score;
}
}
}
finally
{
if (rented is not null)
{
ArrayPool<int>.Shared.Return(rented);
}
}
}
// Schema ID is for cache invalidation of precomputed targets.
// Only includes options that affect folding/bloom, not scoring.
private static uint ComputeSchemaId(PrecomputedFuzzyMatcherOptions o)
{
const uint fnvOffset = 2166136261;
const uint fnvPrime = 16777619;
var h = fnvOffset;
h = unchecked((h ^ FolderSchemaVersion) * fnvPrime);
h = unchecked((h ^ BloomSchemaVersion) * fnvPrime);
h = unchecked((h ^ (uint)(o.RemoveDiacritics ? 1 : 0)) * fnvPrime);
return h;
}
}

View File

@@ -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.
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcherOptions
{
public static PrecomputedFuzzyMatcherOptions Default { get; } = new();
/*
* Bonuses
*/
public int CharMatchBonus { get; init; } = 1;
public int SameCaseBonus { get; init; } = 1;
public int ConsecutiveMultiplier { get; init; } = 5;
public int CamelCaseBonus { get; init; } = 2;
public int StartOfWordBonus { get; init; } = 8;
public int PathSeparatorBonus { get; init; } = 5;
public int WordSeparatorBonus { get; init; } = 4;
public int SeparatorAlignmentBonus { get; init; } = 2;
public int ExactSeparatorBonus { get; init; } = 1;
/*
* Settings
*/
public bool RemoveDiacritics { get; init; } = true;
public bool SkipWordSeparators { get; init; } = true;
public bool IgnoreSameCaseBonusIfQueryIsAllLowercase { get; init; } = true;
}

View File

@@ -1,177 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.CompilerServices;
using ToolGood.Words.Pinyin;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcherWithPinyin : IPrecomputedFuzzyMatcher
{
private readonly IBloomFilter _bloom;
private readonly PrecomputedFuzzyMatcher _core;
private readonly IStringFolder _stringFolder;
private readonly PinyinFuzzyMatcherOptions _pinyin;
public PrecomputedFuzzyMatcherWithPinyin(
PrecomputedFuzzyMatcherOptions coreOptions,
PinyinFuzzyMatcherOptions pinyinOptions,
IStringFolder stringFolder,
IBloomFilter bloom)
{
_pinyin = pinyinOptions;
_stringFolder = stringFolder;
_bloom = bloom;
_core = new PrecomputedFuzzyMatcher(coreOptions, stringFolder, bloom);
SchemaId = CombineSchema(_core.SchemaId, _pinyin);
}
public uint SchemaId { get; }
public FuzzyQuery PrecomputeQuery(string? input)
{
input ??= string.Empty;
var primary = _core.PrecomputeQuery(input);
// Fast exit if effectively off (provider should already filter, but keep robust)
if (!IsPinyinEnabled(_pinyin))
{
return primary;
}
// Match legacy: remove apostrophes for query secondary
var queryForPinyin = _pinyin.RemoveApostrophesForQuery ? RemoveApostrophesIfAny(input) : input;
var pinyin = WordsHelper.GetPinyin(queryForPinyin);
if (string.IsNullOrEmpty(pinyin))
{
return primary;
}
var secondary = _core.PrecomputeQuery(pinyin);
return new FuzzyQuery(
primary.Original,
primary.Folded,
primary.Bloom,
primary.EffectiveLength,
primary.IsAllLowercaseAsciiOrNonLetter,
secondary.Original,
secondary.Folded,
secondary.Bloom,
secondary.EffectiveLength,
secondary.SecondaryIsAllLowercaseAsciiOrNonLetter);
}
public FuzzyTarget PrecomputeTarget(string? input)
{
input ??= string.Empty;
var primary = _core.PrecomputeTarget(input);
if (!IsPinyinEnabled(_pinyin))
{
return primary;
}
// Match legacy: only compute target pinyin when target contains Chinese
if (!ContainsToolGoodChinese(input))
{
return primary;
}
var pinyin = WordsHelper.GetPinyin(input);
if (string.IsNullOrEmpty(pinyin))
{
return primary;
}
var secondary = _core.PrecomputeTarget(pinyin);
return new FuzzyTarget(
primary.Original,
primary.Folded,
primary.Bloom,
secondary.Original,
secondary.Folded,
secondary.Bloom);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target)
=> _core.Score(in query, in target);
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o) => o.Mode switch
{
PinyinMode.Off => false,
PinyinMode.On => true,
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
_ => false,
};
private static bool IsSimplifiedChineseUi()
{
var culture = CultureInfo.CurrentUICulture;
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
}
private static bool ContainsToolGoodChinese(string s)
{
return WordsHelper.HasChinese(s);
}
private static string RemoveApostrophesIfAny(string input)
{
var first = input.IndexOf('\'');
if (first < 0)
{
return input;
}
var removeCount = 1;
for (var i = first + 1; i < input.Length; i++)
{
if (input[i] == '\'')
{
removeCount++;
}
}
return string.Create(input.Length - removeCount, input, static (dst, src) =>
{
var di = 0;
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
if (c == '\'')
{
continue;
}
dst[di++] = c;
}
});
}
private static uint CombineSchema(uint coreSchemaId, PinyinFuzzyMatcherOptions p)
{
const uint fnvOffset = 2166136261;
const uint fnvPrime = 16777619;
var h = fnvOffset;
h = unchecked((h ^ coreSchemaId) * fnvPrime);
h = unchecked((h ^ (uint)p.Mode) * fnvPrime);
h = unchecked((h ^ (p.RemoveApostrophesForQuery ? 1u : 0u)) * fnvPrime);
// bump if you change formatting/conversion behavior
const uint pinyinAlgoVersion = 1;
h = unchecked((h ^ pinyinAlgoVersion) * fnvPrime);
return h;
}
}

View File

@@ -1,163 +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.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class StringFolder : IStringFolder
{
// Cache for diacritic-stripped uppercase characters.
// Benign race: worst case is redundant computation writing the same value.
// 0 = uncached, else cachedChar + 1
private static readonly ushort[] StripCacheUpper = new ushort[char.MaxValue + 1];
public string Fold(string input, bool removeDiacritics)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
if (!removeDiacritics || Ascii.IsValid(input))
{
if (IsAlreadyFoldedAndSlashNormalized(input))
{
return input;
}
return string.Create(input.Length, input, static (dst, src) =>
{
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
dst[i] = c == '\\' ? '/' : char.ToUpperInvariant(c);
}
});
}
return string.Create(input.Length, input, static (dst, src) =>
{
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
var upper = c == '\\' ? '/' : char.ToUpperInvariant(c);
dst[i] = StripDiacriticsFromUpper(upper);
}
});
}
private static bool IsAlreadyFoldedAndSlashNormalized(string input)
{
var sawNonAscii = false;
for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (c == '\\')
{
return false;
}
if ((uint)(c - 'a') <= 'z' - 'a')
{
return false;
}
if (c > 0x7F)
{
sawNonAscii = true;
}
}
if (sawNonAscii)
{
for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (c <= 0x7F)
{
continue;
}
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
if (cat is UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter)
{
return false;
}
}
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static char StripDiacriticsFromUpper(char upper)
{
if (upper <= 0x7F)
{
return upper;
}
// Never attempt normalization on lone UTF-16 surrogates.
if (char.IsSurrogate(upper))
{
return upper;
}
var cachedPlus1 = StripCacheUpper[upper];
if (cachedPlus1 != 0)
{
return (char)(cachedPlus1 - 1);
}
var mapped = StripDiacriticsSlow(upper);
StripCacheUpper[upper] = (ushort)(mapped + 1);
return mapped;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static char StripDiacriticsSlow(char upper)
{
try
{
var baseChar = FirstNonMark(upper, NormalizationForm.FormD);
if (baseChar == '\0' || baseChar == upper)
{
var kd = FirstNonMark(upper, NormalizationForm.FormKD);
if (kd != '\0')
{
baseChar = kd;
}
}
return char.ToUpperInvariant(baseChar == '\0' ? upper : baseChar);
}
catch
{
// Absolute safety: if globalization tables ever throw for some reason,
// degrade gracefully rather than failing hard.
return upper;
}
static char FirstNonMark(char c, NormalizationForm form)
{
var normalized = c.ToString().Normalize(form);
foreach (var ch in normalized)
{
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
if (cat is not (UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark or UnicodeCategory.EnclosingMark))
{
return ch;
}
}
return '\0';
}
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.Core.Common.Text;
internal static class SymbolClassifier
{
// Embedded in .data section - no allocation, no static constructor
private static ReadOnlySpan<byte> Lookup =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
2, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 1, // 32-47: space=2, "=2, '=2, -=2, .=2, /=1
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, // 48-63: :=2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64-79
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, // 80-95: _=2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96-111
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 112-127
];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SymbolKind Classify(char c)
{
return c > 0x7F ? SymbolKind.Other : (SymbolKind)Lookup[c];
}
}

View File

@@ -1,12 +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.Core.Common.Text;
internal enum SymbolKind : byte
{
Other = 0,
PathSeparator = 1,
WordSeparator = 2,
}

View File

@@ -4,8 +4,6 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -15,7 +13,7 @@ using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext, IPrecomputedListItem
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
{
public ExtensionObject<ICommandItem> Model => _commandItemModel;
@@ -24,9 +22,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
private FuzzyTargetCache _titleCache;
private FuzzyTargetCache _subtitleCache;
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
@@ -121,8 +116,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
_itemTitle = model.Title;
Subtitle = model.Subtitle;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
Initialized |= InitializedState.FastInitialized;
}
@@ -256,8 +249,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Subtitle = "Item failed to load";
MoreCommands = [];
_icon = _errorIcon;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
Initialized |= InitializedState.Error;
}
@@ -295,8 +286,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Subtitle = "Item failed to load";
MoreCommands = [];
_icon = _errorIcon;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
Initialized |= InitializedState.Error;
}
@@ -346,14 +335,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
case nameof(Title):
_itemTitle = model.Title;
_titleCache.Invalidate();
break;
case nameof(Subtitle):
var modelSubtitle = model.Subtitle;
this.Subtitle = modelSubtitle;
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
_subtitleCache.Invalidate();
break;
case nameof(Icon):
@@ -428,7 +415,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
// or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK.
_itemTitle = model.Title;
_titleCache.Invalidate();
UpdateProperty(nameof(Title), nameof(Name));
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
@@ -450,7 +436,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private void UpdateTitle(string? title)
{
_itemTitle = title ?? string.Empty;
_titleCache.Invalidate();
UpdateProperty(nameof(Title));
}
@@ -471,12 +456,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(DataPackage));
}
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _titleCache.GetOrUpdate(matcher, Title);
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
protected override void UnsafeCleanup()
{
base.UnsafeCleanup();

View File

@@ -11,8 +11,6 @@ public partial class CommandViewModel : ExtensionObjectViewModel
{
public ExtensionObject<ICommand> Model { get; private set; } = new(null);
public bool IsSet => Model.Unsafe is not null;
protected bool IsInitialized { get; private set; }
protected bool IsFastInitialized { get; private set; }

View File

@@ -3,12 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,8 +16,6 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ContextMenuViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
{
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
public ICommandBarContext? SelectedItem
{
get => field;
@@ -44,9 +39,8 @@ public partial class ContextMenuViewModel : ObservableObject,
private string _lastSearchText = string.Empty;
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
public ContextMenuViewModel()
{
_fuzzyMatcherProvider = fuzzyMatcherProvider;
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
}
@@ -97,14 +91,13 @@ public partial class ContextMenuViewModel : ObservableObject,
.OfType<CommandContextItemViewModel>()
.Where(c => c.ShouldBeVisible);
var query = _fuzzyMatcherProvider.Current.PrecomputeQuery(searchText);
var newResults = InternalListHelpers.FilterList(commands, in query, ScoreFunction);
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private int ScoreFunction(in FuzzyQuery query, CommandContextItemViewModel item)
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrWhiteSpace(query.Original))
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
@@ -114,21 +107,11 @@ public partial class ContextMenuViewModel : ObservableObject,
return 0;
}
var fuzzyMatcher = _fuzzyMatcherProvider.Current;
var title = item.GetTitleTarget(fuzzyMatcher);
var subtitle = item.GetSubtitleTarget(fuzzyMatcher);
var nameMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Title);
var titleScore = fuzzyMatcher.Score(query, title);
var subtitleScore = (fuzzyMatcher.Score(query, subtitle) - 4) / 2;
var descriptionMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Subtitle);
return Max3(titleScore, subtitleScore, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Max3(int a, int b, int c)
{
var m = a > b ? a : b;
return m > c ? m : c;
return new[] { nameMatch, (descriptionMatch - 4) / 2, 0 }.Max();
}
/// <summary>

View File

@@ -1,12 +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.Core.ViewModels;
public enum ListItemType
{
Item,
SectionHeader,
Separator,
}

View File

@@ -24,9 +24,7 @@ public partial class ListItemViewModel : CommandItemViewModel
public string Section { get; private set; } = string.Empty;
public ListItemType Type { get; private set; }
public bool IsInteractive => Type == ListItemType.Item;
public bool IsSectionOrSeparator { get; private set; }
public DetailsViewModel? Details { get; private set; }
@@ -87,17 +85,15 @@ public partial class ListItemViewModel : CommandItemViewModel
UpdateTags(li.Tags);
Section = li.Section ?? string.Empty;
Type = EvaluateType();
UpdateProperty(nameof(Section), nameof(Type), nameof(IsInteractive));
IsSectionOrSeparator = IsSeparator(li);
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
UpdateAccessibleName();
}
private ListItemType EvaluateType()
private bool IsSeparator(IListItem item)
{
return Command.IsSet
? ListItemType.Item
: string.IsNullOrEmpty(Section) ? ListItemType.Separator : ListItemType.SectionHeader;
return item.Command is null;
}
public override void SlowInitializeProperties()
@@ -144,12 +140,12 @@ public partial class ListItemViewModel : CommandItemViewModel
break;
case nameof(model.Section):
Section = model.Section ?? string.Empty;
Type = EvaluateType();
UpdateProperty(nameof(Section), nameof(Type), nameof(IsInteractive));
IsSectionOrSeparator = IsSeparator(model);
UpdateProperty(nameof(Section), nameof(IsSectionOrSeparator));
break;
case nameof(model.Command):
Type = EvaluateType();
UpdateProperty(nameof(Type), nameof(IsInteractive));
IsSectionOrSeparator = IsSeparator(model);
UpdateProperty(nameof(IsSectionOrSeparator));
break;
case nameof(Details):
var extensionDetails = model.Details;

View File

@@ -18,8 +18,6 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
{
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
// row 0
@@ -130,13 +128,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
OnPropertyChanged(nameof(IsColorIntensityVisible));
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
OnPropertyChanged(nameof(EffectiveTintIntensity));
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
OnPropertyChanged(nameof(IsNoBackgroundVisible));
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
OnPropertyChanged(nameof(IsResetButtonVisible));
if (value == ColorizationMode.WindowsAccentColor)
{
@@ -184,19 +179,6 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
{
_settings.CustomThemeColorIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
}
}
public int BackgroundImageTintIntensity
{
get => _settings.BackgroundImageTintIntensity;
set
{
_settings.BackgroundImageTintIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
}
}
@@ -297,108 +279,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
};
}
public int BackdropOpacity
{
get => _settings.BackdropOpacity;
set
{
if (_settings.BackdropOpacity != value)
{
_settings.BackdropOpacity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
Save();
}
}
}
public int BackdropStyleIndex
{
get => (int)_settings.BackdropStyle;
set
{
var newStyle = (BackdropStyle)value;
if (_settings.BackdropStyle != newStyle)
{
_settings.BackdropStyle = newStyle;
OnPropertyChanged();
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
OnPropertyChanged(nameof(IsMicaBackdropDescriptionVisible));
OnPropertyChanged(nameof(IsBackgroundSettingsEnabled));
OnPropertyChanged(nameof(IsBackgroundNotAvailableVisible));
if (!IsBackgroundSettingsEnabled)
{
IsColorizationDetailsExpanded = false;
}
Save();
}
}
}
/// <summary>
/// Gets whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// </summary>
public bool IsBackgroundNotAvailableVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
public BackdropStyle? EffectiveBackdropStyle
{
get
{
// Return style when transparency/blur is visible (not fully opaque Acrylic)
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
// - Acrylic shows effect only when opacity < 100
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
{
return _settings.BackdropStyle;
}
return null;
}
}
public double EffectiveImageOpacity =>
EffectiveBackdropStyle is not null
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
: (BackgroundImageOpacity / 100f);
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
/// <summary>
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
/// </summary>
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
? _settings.BackgroundImageTintIntensity
: _settings.CustomThemeColorIntensity;
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
@@ -406,21 +292,16 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
public Color EffectiveThemeColor =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
? Colors.Transparent
: ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
public Color EffectiveThemeColor => ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
@@ -428,13 +309,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
? null
: ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
{
@@ -448,7 +327,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Reapply();
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
@@ -478,8 +357,6 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
// Theme services recalculates effective color and opacity based on current settings.
EffectiveBackdrop = _themeService.Current.BackdropParameters;
OnPropertyChanged(nameof(EffectiveBackdrop));
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
OnPropertyChanged(nameof(EffectiveThemeColor));
@@ -502,28 +379,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
BackgroundImageBlurAmount = 0;
BackgroundImageFit = BackgroundImageFit.UniformToFill;
BackgroundImageOpacity = 100;
BackgroundImageTintIntensity = 0;
}
[RelayCommand]
private void ResetAppearanceSettings()
{
// Reset theme
Theme = UserTheme.Default;
// Reset backdrop settings
BackdropStyleIndex = (int)BackdropStyle.Acrylic;
BackdropOpacity = 100;
// Reset background image settings
BackgroundImagePath = string.Empty;
ResetBackgroundImageProperties();
// Reset colorization
ColorizationMode = ColorizationMode.None;
ThemeColor = DefaultTintColor;
ColorIntensity = 100;
BackgroundImageTintIntensity = 0;
ColorIntensity = 0;
}
public void Dispose()

View File

@@ -1,41 +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.UI.ViewModels;
/// <summary>
/// Specifies the type of system backdrop controller to use.
/// </summary>
public enum BackdropControllerKind
{
/// <summary>
/// Solid color with alpha transparency (TransparentTintBackdrop).
/// </summary>
Solid,
/// <summary>
/// Desktop Acrylic with default blur (DesktopAcrylicKind.Default).
/// </summary>
Acrylic,
/// <summary>
/// Desktop Acrylic with thinner blur (DesktopAcrylicKind.Thin).
/// </summary>
AcrylicThin,
/// <summary>
/// Mica effect (MicaKind.Base).
/// </summary>
Mica,
/// <summary>
/// Mica alternate/darker variant (MicaKind.BaseAlt).
/// </summary>
MicaAlt,
/// <summary>
/// Custom backdrop implementation.
/// </summary>
Custom,
}

View File

@@ -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.
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the visual backdrop style for the window.
/// </summary>
public enum BackdropStyle
{
/// <summary>
/// Standard desktop acrylic with blur effect.
/// </summary>
Acrylic,
/// <summary>
/// Solid color with alpha transparency (no blur).
/// </summary>
Clear,
/// <summary>
/// Mica effect that samples the desktop wallpaper.
/// </summary>
Mica,
/// <summary>
/// Thinner acrylic variant with more transparency.
/// </summary>
AcrylicThin,
/// <summary>
/// Mica alternate variant (darker).
/// </summary>
MicaAlt,
}

View File

@@ -1,77 +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.UI.ViewModels;
/// <summary>
/// Configuration parameters for a backdrop style.
/// </summary>
public sealed record BackdropStyleConfig
{
/// <summary>
/// Gets the type of system backdrop controller to use.
/// </summary>
public required BackdropControllerKind ControllerKind { get; init; }
/// <summary>
/// Gets the base tint opacity before user adjustments.
/// </summary>
public required float BaseTintOpacity { get; init; }
/// <summary>
/// Gets the base luminosity opacity before user adjustments.
/// </summary>
public required float BaseLuminosityOpacity { get; init; }
/// <summary>
/// Gets the brush type to use for preview approximation.
/// </summary>
public required PreviewBrushKind PreviewBrush { get; init; }
/// <summary>
/// Gets the fixed opacity for styles that don't support user adjustment (e.g., Mica).
/// When <see cref="SupportsOpacity"/> is false, this value is used as the effective opacity.
/// </summary>
public float FixedOpacity { get; init; }
/// <summary>
/// Gets whether this backdrop style supports custom colorization (tint colors).
/// </summary>
public bool SupportsColorization { get; init; } = true;
/// <summary>
/// Gets whether this backdrop style supports custom background images.
/// </summary>
public bool SupportsBackgroundImage { get; init; } = true;
/// <summary>
/// Gets whether this backdrop style supports opacity adjustment.
/// </summary>
public bool SupportsOpacity { get; init; } = true;
/// <summary>
/// Computes the effective tint opacity based on this style's configuration.
/// </summary>
/// <param name="userOpacity">User's backdrop opacity setting (0-1 normalized).</param>
/// <param name="baseTintOpacityOverride">Optional override for base tint opacity (used by colorful theme).</param>
/// <returns>The effective opacity to apply.</returns>
public float ComputeEffectiveOpacity(float userOpacity, float? baseTintOpacityOverride = null)
{
// For styles that don't support opacity (Mica), use FixedOpacity
if (!SupportsOpacity && FixedOpacity > 0)
{
return FixedOpacity;
}
// For Solid: only user opacity matters (controls alpha of solid color)
if (ControllerKind == BackdropControllerKind.Solid)
{
return userOpacity;
}
// For blur effects: multiply base opacity with user opacity
var baseTint = baseTintOpacityOverride ?? BaseTintOpacity;
return baseTint * userOpacity;
}
}

View File

@@ -1,65 +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.UI.ViewModels;
/// <summary>
/// Central registry of backdrop style configurations.
/// </summary>
public static class BackdropStyles
{
private static readonly Dictionary<BackdropStyle, BackdropStyleConfig> Configs = new()
{
[BackdropStyle.Acrylic] = new()
{
ControllerKind = BackdropControllerKind.Acrylic,
BaseTintOpacity = 0.5f,
BaseLuminosityOpacity = 0.9f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.AcrylicThin] = new()
{
ControllerKind = BackdropControllerKind.AcrylicThin,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 0.85f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.Mica] = new()
{
ControllerKind = BackdropControllerKind.Mica,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.96f,
SupportsOpacity = false,
},
[BackdropStyle.MicaAlt] = new()
{
ControllerKind = BackdropControllerKind.MicaAlt,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.98f,
SupportsOpacity = false,
},
[BackdropStyle.Clear] = new()
{
ControllerKind = BackdropControllerKind.Solid,
BaseTintOpacity = 1.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
},
};
/// <summary>
/// Gets the configuration for the specified backdrop style.
/// </summary>
public static BackdropStyleConfig Get(BackdropStyle style) =>
Configs.TryGetValue(style, out var config) ? config : Configs[BackdropStyle.Acrylic];
/// <summary>
/// Gets all registered backdrop styles.
/// </summary>
public static IEnumerable<BackdropStyle> All => Configs.Keys;
}

View File

@@ -8,7 +8,6 @@ using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.Ext.Apps.Programs;
@@ -25,7 +24,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a <see cref="ListPage"/>.
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
/// </summary>
public sealed partial class MainListPage : DynamicListPage,
public partial class MainListPage : DynamicListPage,
IRecipient<ClearSearchMessage>,
IRecipient<UpdateFallbackItemsMessage>, IDisposable
{
@@ -33,18 +32,13 @@ public sealed partial class MainListPage : DynamicListPage,
private readonly AliasManager _aliasManager;
private readonly SettingsModel _settings;
private readonly AppStateModel _appStateModel;
private readonly ScoringFunction<IListItem> _scoringFunction;
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
private RoScored<IListItem>[]? _filteredItems;
private RoScored<IListItem>[]? _filteredApps;
private List<Scored<IListItem>>? _filteredItems;
private List<Scored<IListItem>>? _filteredApps;
// Keep as IEnumerable for deferred execution. Fallback item titles are updated
// asynchronously, so scoring must happen lazily when GetItems is called.
private IEnumerable<RoScored<IListItem>>? _scoredFallbackItems;
private IEnumerable<RoScored<IListItem>>? _fallbackItems;
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
private IEnumerable<Scored<IListItem>>? _fallbackItems;
private bool _includeApps;
private bool _filteredItemsIncludesApps;
private int _appResultLimit = 10;
@@ -54,12 +48,7 @@ public sealed partial class MainListPage : DynamicListPage,
private CancellationTokenSource? _cancellationTokenSource;
public MainListPage(
TopLevelCommandManager topLevelCommandManager,
SettingsModel settings,
AliasManager aliasManager,
AppStateModel appStateModel,
IFuzzyMatcherProvider fuzzyMatcherProvider)
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
{
Title = Resources.builtin_home_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
@@ -69,10 +58,6 @@ public sealed partial class MainListPage : DynamicListPage,
_aliasManager = aliasManager;
_appStateModel = appStateModel;
_tlcManager = topLevelCommandManager;
_fuzzyMatcherProvider = fuzzyMatcherProvider;
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
@@ -205,7 +190,8 @@ public sealed partial class MainListPage : DynamicListPage,
public override void UpdateSearchText(string oldSearch, string newSearch)
{
var stopwatch = Stopwatch.StartNew();
var timer = new Stopwatch();
timer.Start();
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
@@ -368,14 +354,15 @@ public sealed partial class MainListPage : DynamicListPage,
if (_includeApps)
{
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
// We need to remove pinned apps from allNewApps so they don't show twice.
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
if (pinnedApps.Length > 0)
{
newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0);
newApps = allNewApps.Where(w =>
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
}
else
{
@@ -389,10 +376,11 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
var searchQuery = _fuzzyMatcherProvider.Current.PrecomputeQuery(SearchText);
var history = _appStateModel.RecentCommands!;
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
// Produce a list of everything that matches the current filter.
_filteredItems = InternalListHelpers.FilterListWithScores(newFilteredItems, searchQuery, _scoringFunction);
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
if (token.IsCancellationRequested)
{
@@ -400,14 +388,21 @@ public sealed partial class MainListPage : DynamicListPage,
}
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
if (token.IsCancellationRequested)
{
return;
}
_fallbackItems = InternalListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], searchQuery, _fallbackScoringFunction);
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
if (token.IsCancellationRequested)
{
return;
}
Func<string, IListItem, int> scoreFallbackItem = (a, b) => { return ScoreFallbackItem(a, b, _settings.FallbackRanks); };
_fallbackItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], SearchText, scoreFallbackItem)];
if (token.IsCancellationRequested)
{
@@ -417,7 +412,18 @@ public sealed partial class MainListPage : DynamicListPage,
// Produce a list of filtered apps with the appropriate limit
if (newApps.Any())
{
_filteredApps = InternalListHelpers.FilterListWithScores(newApps, searchQuery, _scoringFunction);
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
if (token.IsCancellationRequested)
{
return;
}
// We'll apply this limit in the GetItems method after merging with commands
// but we need to know the limit now to avoid re-scoring apps
var appLimit = AllAppsCommandProvider.TopLevelResultLimit;
_filteredApps = [.. scoredApps];
if (token.IsCancellationRequested)
{
@@ -425,15 +431,10 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
RaiseItemsChanged();
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
stopwatch.Stop();
timer.Stop();
Logger.LogDebug($"Filter with '{newSearch}' in {timer.ElapsedMilliseconds}ms");
}
}
@@ -477,11 +478,7 @@ public sealed partial class MainListPage : DynamicListPage,
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
// fact that we want fallback handlers down-weighted, so that they don't
// _always_ show up first.
internal static int ScoreTopLevelItem(
in FuzzyQuery query,
IListItem topLevelOrAppItem,
IRecentCommandsManager history,
IPrecomputedFuzzyMatcher precomputedFuzzyMatcher)
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
{
var title = topLevelOrAppItem.Title;
if (string.IsNullOrWhiteSpace(title))
@@ -489,80 +486,94 @@ public sealed partial class MainListPage : DynamicListPage,
return 0;
}
var isWhiteSpace = string.IsNullOrWhiteSpace(query);
var isFallback = false;
var isAliasSubstringMatch = false;
var isAliasMatch = false;
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
FuzzyTarget? extensionDisplayNameTarget = null;
var extensionDisplayName = string.Empty;
if (topLevelOrAppItem is TopLevelViewModel topLevel)
{
isFallback = topLevel.IsFallback;
extensionDisplayNameTarget = topLevel.GetExtensionNameTarget(precomputedFuzzyMatcher);
if (topLevel.HasAlias)
{
var alias = topLevel.AliasText;
isAliasMatch = alias == query.Original;
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query.Original, StringComparison.CurrentCultureIgnoreCase);
isAliasMatch = alias == query;
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query, StringComparison.CurrentCultureIgnoreCase);
}
extensionDisplayName = topLevel.ExtensionHost?.Extension?.PackageDisplayName ?? string.Empty;
}
// Handle whitespace query separately - FuzzySearch doesn't handle it well
if (string.IsNullOrWhiteSpace(query.Original))
// StringMatcher.FuzzySearch will absolutely BEEF IT if you give it a
// whitespace-only query.
//
// in that scenario, we'll just use a simple string contains for the
// query. Maybe someone is really looking for things with a space in
// them, I don't know.
// Title:
// * whitespace query: 1 point
// * otherwise full weight match
var nameMatch = isWhiteSpace ?
(title.Contains(query) ? 1 : 0) :
FuzzyStringMatcher.ScoreFuzzy(query, title);
// Subtitle:
// * whitespace query: 1/2 point
// * otherwise ~half weight match. Minus a bit, because subtitles tend to be longer
var descriptionMatch = isWhiteSpace ?
(topLevelOrAppItem.Subtitle.Contains(query) ? .5 : 0) :
(FuzzyStringMatcher.ScoreFuzzy(query, topLevelOrAppItem.Subtitle) - 4) / 2.0;
// Extension title: despite not being visible, give the extension name itself some weight
// * whitespace query: 0 points
// * otherwise more weight than a subtitle, but not much
var extensionTitleMatch = isWhiteSpace ? 0 : FuzzyStringMatcher.ScoreFuzzy(query, extensionDisplayName) / 1.5;
var scores = new[]
{
return ScoreWhitespaceQuery(query.Original, title, topLevelOrAppItem.Subtitle, isFallback);
}
nameMatch,
descriptionMatch,
isFallback ? 1 : 0, // Always give fallbacks a chance
};
var max = scores.Max();
// Get precomputed targets
var (titleTarget, subtitleTarget) = topLevelOrAppItem is IPrecomputedListItem precomputedItem
? (precomputedItem.GetTitleTarget(precomputedFuzzyMatcher), precomputedItem.GetSubtitleTarget(precomputedFuzzyMatcher))
: (precomputedFuzzyMatcher.PrecomputeTarget(title), precomputedFuzzyMatcher.PrecomputeTarget(topLevelOrAppItem.Subtitle));
// Score components
var nameScore = precomputedFuzzyMatcher.Score(query, titleTarget);
var descriptionScore = (precomputedFuzzyMatcher.Score(query, subtitleTarget) - 4) / 2.0;
var extensionScore = extensionDisplayNameTarget is { } extTarget ? precomputedFuzzyMatcher.Score(query, extTarget) / 1.5 : 0;
// Take best match from title/description/fallback, then add extension score
// Extension adds to max so items matching both title AND extension bubble up
var baseScore = Math.Max(Math.Max(nameScore, descriptionScore), isFallback ? 1 : 0);
var matchScore = baseScore + extensionScore;
// _Add_ the extension name. This will bubble items that match both
// title and extension name up above ones that just match title.
// e.g. "git" will up-weight "GitHub searches" from the GitHub extension
// above "git" from "whatever"
max = max + extensionTitleMatch;
// Apply a penalty to fallback items so they rank below direct matches.
// Fallbacks that dynamically match queries (like RDP connections) should
// appear after apps and direct command matches.
if (isFallback && matchScore > 1)
if (isFallback && max > 1)
{
// Reduce fallback scores by 50% to prioritize direct matches
matchScore = matchScore * 0.5;
max = max * 0.5;
}
// Alias matching: exact match is overwhelming priority, substring match adds a small boost
var aliasBoost = isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0);
var totalMatch = matchScore + aliasBoost;
var matchSomething = max
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
// Apply scaling and history boost only if we matched something real
var finalScore = totalMatch * 10;
if (totalMatch > 0)
// If we matched title, subtitle, or alias (something real), then
// here we add the recent command weight boost
//
// Otherwise something like `x` will still match everything you've run before
var finalScore = matchSomething * 10;
if (matchSomething > 0)
{
finalScore += history.GetCommandHistoryWeight(id);
var recentWeightBoost = history.GetCommandHistoryWeight(id);
finalScore += recentWeightBoost;
}
return (int)finalScore;
}
private static int ScoreWhitespaceQuery(string query, string title, string subtitle, bool isFallback)
{
// Simple contains check for whitespace queries
var nameMatch = title.Contains(query, StringComparison.Ordinal) ? 1.0 : 0;
var descriptionMatch = subtitle.Contains(query, StringComparison.Ordinal) ? 0.5 : 0;
var baseScore = Math.Max(Math.Max(nameMatch, descriptionMatch), isFallback ? 1 : 0);
return (int)(baseScore * 10);
}
private static int ScoreFallbackItem(IListItem topLevelOrAppItem, string[] fallbackRanks)
internal static int ScoreFallbackItem(string query, IListItem topLevelOrAppItem, string[] fallbackRanks)
{
// Default to 1 so it always shows in list.
var finalScore = 1;

View File

@@ -4,7 +4,6 @@
#pragma warning disable IDE0007 // Use implicit type
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,10 +16,10 @@ internal static class MainListPageResultFactory
/// applying an application result limit and filtering fallback items as needed.
/// </summary>
public static IListItem[] Create(
IList<RoScored<IListItem>>? filteredItems,
IList<RoScored<IListItem>>? scoredFallbackItems,
IList<RoScored<IListItem>>? filteredApps,
IList<RoScored<IListItem>>? fallbackItems,
IList<Scored<IListItem>>? filteredItems,
IList<Scored<IListItem>>? scoredFallbackItems,
IList<Scored<IListItem>>? filteredApps,
IList<Scored<IListItem>>? fallbackItems,
int appResultLimit)
{
if (appResultLimit < 0)
@@ -148,7 +147,7 @@ internal static class MainListPageResultFactory
return result;
}
private static int GetNonEmptyFallbackItemsCount(IList<RoScored<IListItem>>? fallbackItems)
private static int GetNonEmptyFallbackItemsCount(IList<Scored<IListItem>>? fallbackItems)
{
int fallbackItemsCount = 0;

View File

@@ -22,7 +22,6 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
public partial double BackgroundImageOpacity { get; private set; }
[ObservableProperty]
@@ -40,30 +39,6 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
[ObservableProperty]
public partial bool ShowBackgroundImage { get; private set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EffectiveBackdropStyle))]
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
public partial BackdropStyle BackdropStyle { get; private set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(EffectiveBackdropStyle))]
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
public partial float BackdropOpacity { get; private set; } = 1.0f;
// Returns null when no transparency needed (BlurImageControl uses this to decide source type)
public BackdropStyle? EffectiveBackdropStyle =>
BackdropStyle == BackdropStyle.Clear ||
BackdropStyle == BackdropStyle.Mica ||
BackdropOpacity < 1.0f
? BackdropStyle
: null;
// When transparency is enabled, use square root curve so image stays visible longer as backdrop fades
public double EffectiveImageOpacity =>
EffectiveBackdropStyle is not null
? BackgroundImageOpacity * Math.Sqrt(BackdropOpacity)
: BackgroundImageOpacity;
public MainWindowViewModel(IThemeService themeService)
{
_themeService = themeService;
@@ -83,9 +58,6 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
BackdropStyle = _themeService.Current.BackdropParameters.Style;
BackdropOpacity = _themeService.Current.BackdropOpacity;
ShowBackgroundImage = BackgroundImageSource != null;
});
}

View File

@@ -1,21 +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.UI.ViewModels;
/// <summary>
/// Specifies the brush type to use for backdrop preview approximation.
/// </summary>
public enum PreviewBrushKind
{
/// <summary>
/// SolidColorBrush with computed alpha.
/// </summary>
Solid,
/// <summary>
/// AcrylicBrush with blur effect.
/// </summary>
Acrylic,
}

View File

@@ -285,42 +285,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Built-in.
/// </summary>
public static string builtin_extension_name {
get {
return ResourceManager.GetString("builtin_extension_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}, {1} commands.
/// </summary>
public static string builtin_extension_subtext {
get {
return ResourceManager.GetString("builtin_extension_subtext", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}, {1}.
/// </summary>
public static string builtin_extension_subtext_disabled {
get {
return ResourceManager.GetString("builtin_extension_subtext_disabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0}, {1} commands, {2} fallback commands.
/// </summary>
public static string builtin_extension_subtext_with_fallback {
get {
return ResourceManager.GetString("builtin_extension_subtext_with_fallback", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Home.
/// </summary>

View File

@@ -126,21 +126,6 @@
<data name="builtin_display_name" xml:space="preserve">
<value>Built-in commands</value>
</data>
<data name="builtin_extension_name" xml:space="preserve">
<value>Built-in</value>
</data>
<data name="builtin_extension_subtext" xml:space="preserve">
<value>{0}, {1} commands</value>
<comment>{0}=extension name, {1}=number of commands</comment>
</data>
<data name="builtin_extension_subtext_with_fallback" xml:space="preserve">
<value>{0}, {1} commands, {2} fallback commands</value>
<comment>{0}=extension name, {1}=number of commands, {2} number of fallback commands</comment>
</data>
<data name="builtin_extension_subtext_disabled" xml:space="preserve">
<value>{0}, {1}</value>
<comment>{0}=extension name, {1}=message</comment>
</data>
<data name="builtin_log_subtitle" xml:space="preserve">
<value>View log messages</value>
</data>

View File

@@ -3,8 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.Common.Services;
@@ -17,9 +15,6 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ProviderSettingsViewModel : ObservableObject
{
private static readonly IconInfoViewModel EmptyIcon = new(null);
private static readonly CompositeFormat ExtensionSubtextFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext);
private static readonly CompositeFormat ExtensionSubtextWithFallbackFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext_with_fallback);
private static readonly CompositeFormat ExtensionSubtextDisabledFormat = CompositeFormat.Parse(Resources.builtin_extension_subtext_disabled);
private readonly CommandProviderWrapper _provider;
private readonly ProviderSettings _providerSettings;
@@ -44,13 +39,13 @@ public partial class ProviderSettingsViewModel : ObservableObject
public string DisplayName => _provider.DisplayName;
public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? Resources.builtin_extension_name;
public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? "Built-in";
public string ExtensionSubtext => IsEnabled ?
HasFallbackCommands ?
string.Format(CultureInfo.CurrentCulture, ExtensionSubtextWithFallbackFormat, ExtensionName, TopLevelCommands.Count, _provider.FallbackItems?.Length ?? 0) :
string.Format(CultureInfo.CurrentCulture, ExtensionSubtextFormat, ExtensionName, TopLevelCommands.Count) :
string.Format(CultureInfo.CurrentCulture, ExtensionSubtextDisabledFormat, ExtensionName, Resources.builtin_disabled_extension);
$"{ExtensionName}, {TopLevelCommands.Count} commands, {_provider.FallbackItems?.Length} fallback commands" :
$"{ExtensionName}, {TopLevelCommands.Count} commands" :
$"{ExtensionName}, {Resources.builtin_disabled_extension}";
[MemberNotNullWhen(true, nameof(Extension))]
public bool IsFromExtension => _provider.Extension is not null;

View File

@@ -0,0 +1,9 @@
// 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 Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);

View File

@@ -1,29 +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 Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Parameters for configuring the window backdrop appearance.
/// </summary>
/// <param name="TintColor">The tint color applied to the backdrop.</param>
/// <param name="FallbackColor">The fallback color when backdrop effects are unavailable.</param>
/// <param name="EffectiveOpacity">
/// The effective opacity for the backdrop, pre-computed by the theme provider.
/// For Acrylic style: TintOpacity * BackdropOpacity.
/// For Clear style: BackdropOpacity (controls the solid color alpha).
/// </param>
/// <param name="EffectiveLuminosityOpacity">
/// The effective luminosity opacity for Acrylic backdrop, pre-computed by the theme provider.
/// Computed as LuminosityOpacity * BackdropOpacity.
/// </param>
/// <param name="Style">The backdrop style (Acrylic or Clear).</param>
public sealed record BackdropParameters(
Color TintColor,
Color FallbackColor,
float EffectiveOpacity,
float EffectiveLuminosityOpacity,
BackdropStyle Style = BackdropStyle.Acrylic);

View File

@@ -51,23 +51,12 @@ public sealed class ThemeSnapshot
public required double BackgroundImageOpacity { get; init; }
/// <summary>
/// Gets the effective backdrop parameters based on current settings and theme.
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
/// </summary>
/// <returns>The resolved <c>BackdropParameters</c> to apply.</returns>
public required BackdropParameters BackdropParameters { get; init; }
/// <summary>
/// Gets the raw backdrop opacity setting (0-1 range).
/// Used for determining if transparency is enabled and for image opacity calculations.
/// </summary>
public required float BackdropOpacity { get; init; }
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
public required AcrylicBackdropParameters BackdropParameters { get; init; }
public required int BlurAmount { get; init; }
public required float BackgroundBrightness { get; init; }
/// <summary>
/// Gets whether colorization is active (accent color, custom color, or image mode).
/// </summary>
public required bool HasColorization { get; init; }
}

View File

@@ -74,8 +74,6 @@ public partial class SettingsModel : ObservableObject
public int CustomThemeColorIntensity { get; set; } = 100;
public int BackgroundImageTintIntensity { get; set; }
public int BackgroundImageOpacity { get; set; } = 20;
public int BackgroundImageBlurAmount { get; set; }
@@ -86,10 +84,6 @@ public partial class SettingsModel : ObservableObject
public string? BackgroundImagePath { get; set; }
public BackdropStyle BackdropStyle { get; set; }
public int BackdropOpacity { get; set; } = 100;
// END SETTINGS
///////////////////////////////////////////////////////////////////////////

View File

@@ -3,11 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
@@ -19,8 +16,7 @@ using WyHash;
namespace Microsoft.CmdPal.UI.ViewModels;
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider, IPrecomputedListItem
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
{
private readonly SettingsModel _settings;
private readonly ProviderSettings _providerSettings;
@@ -38,10 +34,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private HotkeySettings? _hotkey;
private IIconInfo? _initialIcon;
private FuzzyTargetCache _titleCache;
private FuzzyTargetCache _subtitleCache;
private FuzzyTargetCache _extensionNameCache;
private CommandAlias? Alias { get; set; }
public bool IsFallback { get; private set; }
@@ -184,8 +176,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
}
}
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
public TopLevelViewModel(
CommandItemViewModel item,
bool isFallback,
@@ -240,15 +230,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
{
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
if (e.PropertyName is nameof(CommandItemViewModel.Title) or nameof(CommandItemViewModel.Name))
{
_titleCache.Invalidate();
}
else if (e.PropertyName is nameof(CommandItemViewModel.Subtitle))
{
_subtitleCache.Invalidate();
}
if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
{
GenerateId();
@@ -439,18 +420,4 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
};
}
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _titleCache.GetOrUpdate(matcher, Title);
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
public FuzzyTarget GetExtensionNameTarget(IPrecomputedFuzzyMatcher matcher)
=> _extensionNameCache.GetOrUpdate(matcher, ExtensionName);
private string GetDebuggerDisplay()
{
return ToString();
}
}

View File

@@ -6,14 +6,12 @@ using ManagedCommon;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
@@ -69,13 +67,11 @@ public partial class App : Application, IDisposable
/// </summary>
public App()
{
var appInfoService = new ApplicationInfoService();
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default, appInfoService);
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default);
#endif
Services = ConfigureServices(appInfoService);
Services = ConfigureServices();
IconCacheProvider.Initialize(Services);
@@ -96,9 +92,6 @@ public partial class App : Application, IDisposable
// This way, log statements from the core project will be captured by the PT logs
var logWrapper = new LogWrapper();
CoreLogger.InitializeLogger(logWrapper);
// Now that CoreLogger is initialized, initialize the logger delegate in ApplicationInfoService
appInfoService.SetLogDirectory(() => Logger.CurrentVersionLogDirectoryPath);
}
/// <summary>
@@ -116,7 +109,7 @@ public partial class App : Application, IDisposable
/// <summary>
/// Configures the services for the application
/// </summary>
private static ServiceProvider ConfigureServices(IApplicationInfoService appInfoService)
private static ServiceProvider ConfigureServices()
{
// TODO: It's in the Labs feed, but we can use Sergio's AOT-friendly source generator for this: https://github.com/CommunityToolkit/Labs-Windows/discussions/463
ServiceCollection services = new();
@@ -127,7 +120,7 @@ public partial class App : Application, IDisposable
AddBuiltInCommands(services);
AddCoreServices(services, appInfoService);
AddCoreServices(services);
AddUIServices(services, dispatcherQueue);
@@ -178,7 +171,6 @@ public partial class App : Application, IDisposable
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
}
private static void AddUIServices(ServiceCollection services, DispatcherQueue dispatcherQueue)
@@ -204,11 +196,9 @@ public partial class App : Application, IDisposable
services.AddIconServices(dispatcherQueue);
}
private static void AddCoreServices(ServiceCollection services, IApplicationInfoService appInfoService)
private static void AddCoreServices(ServiceCollection services)
{
// Core services
services.AddSingleton(appInfoService);
services.AddSingleton<IExtensionService, ExtensionService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
@@ -216,9 +206,6 @@ public partial class App : Application, IDisposable
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
services.AddSingleton<IFuzzyMatcherProvider, FuzzyMatcherProvider>(
_ => new FuzzyMatcherProvider(new PrecomputedFuzzyMatcherOptions(), new PinyinFuzzyMatcherOptions()));
// ViewModels
services.AddSingleton<ShellViewModel>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();

View File

@@ -288,6 +288,7 @@ internal sealed partial class BlurImageControl : Control
_effectBrush?.Dispose();
_effectBrush = effectFactory.CreateBrush();
// Set initial source
if (ImageSource is not null)
{
_imageBrush ??= _compositor.CreateSurfaceBrush();

View File

@@ -16,38 +16,24 @@
CornerRadius="8"
Translation="0,0,8">
<Grid>
<!-- Clear style: SolidColorBrush with computed alpha (window backdrop) -->
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
Visibility="{x:Bind ClearVisibility, Mode=OneWay}">
<Border.Background>
<SolidColorBrush Color="{x:Bind EffectiveClearColor, Mode=OneWay}" />
</Border.Background>
</Border>
<!-- Acrylic/Mica style: AcrylicBrush with effective opacity (window backdrop) -->
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="1"
Visibility="{x:Bind AcrylicVisibility, Mode=OneWay}">
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
<Border.Background>
<AcrylicBrush
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
TintOpacity="{x:Bind PreviewEffectiveOpacity, Mode=OneWay}" />
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
</Border.Background>
</Border>
<!-- Background image (inside window, on top of backdrop) -->
<local:BlurImageControl
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
ImageOpacity="{x:Bind PreviewBackgroundImageOpacity, Mode=OneWay}"
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
IsHitTestVisible="False"

View File

@@ -12,11 +12,13 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandPalettePreview : UserControl
{
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color), OnBackdropPropertyChanged));
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, OnBackgroundImageSourceChanged));
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(1.0));
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, PropertyChangedCallback));
public static readonly DependencyProperty PreviewBackgroundImageOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageOpacity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
@@ -28,18 +30,7 @@ public sealed partial class CommandPalettePreview : UserControl
public static readonly DependencyProperty PreviewBackgroundImageTintIntensityProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageTintIntensity), typeof(int), typeof(CommandPalettePreview), new PropertyMetadata(0));
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed, OnVisibilityPropertyChanged));
public static readonly DependencyProperty PreviewBackdropStyleProperty = DependencyProperty.Register(nameof(PreviewBackdropStyle), typeof(BackdropStyle?), typeof(CommandPalettePreview), new PropertyMetadata(null, OnVisibilityPropertyChanged));
public static readonly DependencyProperty PreviewEffectiveOpacityProperty = DependencyProperty.Register(nameof(PreviewEffectiveOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(1.0, OnBackdropPropertyChanged));
// Computed read-only dependency properties
public static readonly DependencyProperty EffectiveClearColorProperty = DependencyProperty.Register(nameof(EffectiveClearColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
public static readonly DependencyProperty AcrylicVisibilityProperty = DependencyProperty.Register(nameof(AcrylicVisibility), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Visible));
public static readonly DependencyProperty ClearVisibilityProperty = DependencyProperty.Register(nameof(ClearVisibility), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty ShowBackgroundImageProperty = DependencyProperty.Register(nameof(ShowBackgroundImage), typeof(Visibility), typeof(CommandPalettePreview), new PropertyMetadata(Visibility.Collapsed));
public BackgroundImageFit PreviewBackgroundImageFit
{
@@ -47,6 +38,12 @@ public sealed partial class CommandPalettePreview : UserControl
set { SetValue(PreviewBackgroundImageFitProperty, value); }
}
public double PreviewBackgroundOpacity
{
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
set { SetValue(PreviewBackgroundOpacityProperty, value); }
}
public Color PreviewBackgroundColor
{
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
@@ -59,10 +56,10 @@ public sealed partial class CommandPalettePreview : UserControl
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
}
public double PreviewBackgroundImageOpacity
public int PreviewBackgroundImageOpacity
{
get => (double)GetValue(PreviewBackgroundImageOpacityProperty);
set => SetValue(PreviewBackgroundImageOpacityProperty, value);
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
}
public double PreviewBackgroundImageBrightness
@@ -95,48 +92,12 @@ public sealed partial class CommandPalettePreview : UserControl
set => SetValue(ShowBackgroundImageProperty, value);
}
public BackdropStyle? PreviewBackdropStyle
{
get => (BackdropStyle?)GetValue(PreviewBackdropStyleProperty);
set => SetValue(PreviewBackdropStyleProperty, value);
}
/// <summary>
/// Gets or sets the effective opacity for the backdrop, pre-computed by the theme provider.
/// For Acrylic style: used directly as TintOpacity.
/// For Clear style: used to compute the alpha channel of the solid color.
/// </summary>
public double PreviewEffectiveOpacity
{
get => (double)GetValue(PreviewEffectiveOpacityProperty);
set => SetValue(PreviewEffectiveOpacityProperty, value);
}
// Computed read-only properties
public Color EffectiveClearColor
{
get => (Color)GetValue(EffectiveClearColorProperty);
private set => SetValue(EffectiveClearColorProperty, value);
}
public Visibility AcrylicVisibility
{
get => (Visibility)GetValue(AcrylicVisibilityProperty);
private set => SetValue(AcrylicVisibilityProperty, value);
}
public Visibility ClearVisibility
{
get => (Visibility)GetValue(ClearVisibilityProperty);
private set => SetValue(ClearVisibilityProperty, value);
}
public CommandPalettePreview()
{
InitializeComponent();
}
private static void OnBackgroundImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not CommandPalettePreview preview)
{
@@ -146,46 +107,7 @@ public sealed partial class CommandPalettePreview : UserControl
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
}
private static void OnBackdropPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not CommandPalettePreview preview)
{
return;
}
preview.UpdateComputedClearColor();
}
private static void OnVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not CommandPalettePreview preview)
{
return;
}
preview.UpdateComputedVisibilityProperties();
preview.UpdateComputedClearColor();
}
private void UpdateComputedClearColor()
{
EffectiveClearColor = Color.FromArgb(
(byte)(PreviewEffectiveOpacity * 255),
PreviewBackgroundColor.R,
PreviewBackgroundColor.G,
PreviewBackgroundColor.B);
}
private void UpdateComputedVisibilityProperties()
{
var config = BackdropStyles.Get(PreviewBackdropStyle ?? BackdropStyle.Acrylic);
// Show backdrop effect based on style (on top of any background image)
AcrylicVisibility = config.PreviewBrush == PreviewBrushKind.Acrylic
? Visibility.Visible : Visibility.Collapsed;
ClearVisibility = config.PreviewBrush == PreviewBrushKind.Solid
? Visibility.Visible : Visibility.Collapsed;
}
private double ToOpacity(int value) => value / 100.0;
private double ToTintIntensity(int value) => value / 100.0;

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