mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-14 15:19:50 +01:00
Compare commits
35 Commits
async-cpp-
...
dev/mjolle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f8ac0b06 | ||
|
|
09bdbfac38 | ||
|
|
603ac55f8a | ||
|
|
3f5418132d | ||
|
|
e935faf08c | ||
|
|
eb5f4c6bd0 | ||
|
|
658f90d6f8 | ||
|
|
12bfa13b72 | ||
|
|
54d1ab3b70 | ||
|
|
3f3e04086e | ||
|
|
3b874a9567 | ||
|
|
7a86543c8d | ||
|
|
67a013f729 | ||
|
|
521d34f1eb | ||
|
|
a02a5a9736 | ||
|
|
1e25d17920 | ||
|
|
4959273875 | ||
|
|
095961402b | ||
|
|
7477b561a1 | ||
|
|
82671661a8 | ||
|
|
99a6c8c74e | ||
|
|
285183899f | ||
|
|
d3d39f91dc | ||
|
|
10f953f684 | ||
|
|
54c5d7b55d | ||
|
|
94a699d00b | ||
|
|
fa4bc0a397 | ||
|
|
303be86d86 | ||
|
|
ec423177da | ||
|
|
f78ec0c8e5 | ||
|
|
a1e8b6aca9 | ||
|
|
81aeb74fda | ||
|
|
b5ae2efc0d | ||
|
|
ba9585a663 | ||
|
|
5f273c7be6 |
7
.github/actions/spell-check/allow/code.txt
vendored
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -38,6 +38,7 @@ Gbps
|
||||
gcode
|
||||
Heatshrink
|
||||
Mbits
|
||||
Kbits
|
||||
MBs
|
||||
mkv
|
||||
msix
|
||||
@@ -97,6 +98,7 @@ Yubico
|
||||
Perplexity
|
||||
Groq
|
||||
svgl
|
||||
devhome
|
||||
|
||||
# KEYS
|
||||
|
||||
@@ -322,6 +324,7 @@ REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
@@ -342,3 +345,7 @@ reportbug
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
# Performance counter keys
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
3
.github/actions/spell-check/allow/names.txt
vendored
3
.github/actions/spell-check/allow/names.txt
vendored
@@ -192,6 +192,7 @@ ycv
|
||||
yeelam
|
||||
Yuniardi
|
||||
yuyoyuppe
|
||||
zadjii
|
||||
Zeol
|
||||
Zhao
|
||||
Zhaopeng
|
||||
@@ -228,6 +229,7 @@ regedit
|
||||
roslyn
|
||||
Skia
|
||||
Spotify
|
||||
taskmgr
|
||||
tldr
|
||||
Vanara
|
||||
wangyi
|
||||
@@ -243,4 +245,3 @@ xamlstyler
|
||||
Xavalon
|
||||
Xbox
|
||||
Youdao
|
||||
zadjii
|
||||
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -111,6 +111,7 @@
|
||||
^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$
|
||||
|
||||
4
.github/actions/spell-check/expect.txt
vendored
4
.github/actions/spell-check/expect.txt
vendored
@@ -197,6 +197,7 @@ Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
carlos
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
@@ -217,6 +218,7 @@ certmgr
|
||||
cfp
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
chatasweetie
|
||||
checkmarks
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
@@ -1527,6 +1529,7 @@ randi
|
||||
RAquadrant
|
||||
rasterization
|
||||
Rasterize
|
||||
rasterizing
|
||||
RAWINPUTDEVICE
|
||||
RAWINPUTHEADER
|
||||
RAWMODE
|
||||
@@ -2241,6 +2244,7 @@ YSpeed
|
||||
YStr
|
||||
YTimer
|
||||
YVIRTUALSCREEN
|
||||
zamora
|
||||
ZEROINIT
|
||||
zonability
|
||||
zonable
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
<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 >:{ -->
|
||||
|
||||
@@ -196,6 +196,10 @@
|
||||
<Folder Name="/modules/CommandPalette/">
|
||||
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
|
||||
<Project Path="src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj" Id="0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8" />
|
||||
<Project Path="src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
|
||||
@@ -219,6 +223,10 @@
|
||||
<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" />
|
||||
@@ -271,16 +279,6 @@
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Core/">
|
||||
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Microsoft.CmdPal.Core.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Extension SDK/">
|
||||
<Project Path="src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
|
||||
30
README.md
30
README.md
@@ -103,10 +103,38 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.97.1 (January 2026)**
|
||||
|
||||
**Version 0.97.2 (Feb 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
|
||||
|
||||
197
doc/devdocs/events.md
Normal file
197
doc/devdocs/events.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# 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 user‑generated 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
|
||||
@@ -88,7 +88,7 @@
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>inc;..\..\src\;..\..\src\common\Telemetry;telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalOptions>/await /Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
|
||||
<AdditionalOptions>/Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
</ClCompile>
|
||||
|
||||
@@ -57,7 +57,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
|
||||
auto state = UpdateState::read();
|
||||
|
||||
const auto new_version_info = get_github_version_info_async().get();
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
@@ -76,7 +76,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = download_new_version(std::get<new_version_download_info>(*new_version_info)).get();
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
|
||||
@@ -34,8 +34,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
}
|
||||
|
||||
// Check if model is in catalog
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (!isInCatalog)
|
||||
if (!EnsureModelInCatalog(modelId))
|
||||
{
|
||||
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
@@ -43,15 +42,28 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
}
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
var isLoaded = EnsureModelLoadedWithRefresh(modelId);
|
||||
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 = _foundryClient.GetServiceUri();
|
||||
var baseUri = client.GetServiceUri();
|
||||
if (baseUri == null && TryRefreshClient("Service URI was not available"))
|
||||
{
|
||||
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.";
|
||||
@@ -124,6 +136,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
_serviceUrl = await _foundryClient.GetServiceUrl().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,4 +166,75 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace // Strings in this namespace should not be localized
|
||||
|
||||
namespace updating
|
||||
{
|
||||
std::future<bool> uninstall_previous_msix_version_async()
|
||||
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async()
|
||||
{
|
||||
winrt::Windows::Management::Deployment::PackageManager package_manager;
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <common/version/helper.h>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
std::future<bool> uninstall_previous_msix_version_async();
|
||||
}
|
||||
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <winrt/Windows.System.h>
|
||||
|
||||
#include <wil/resource.h>
|
||||
#include <wil/coroutine.h>
|
||||
|
||||
#endif //PCH_H
|
||||
|
||||
|
||||
@@ -82,11 +82,7 @@ namespace updating
|
||||
// prevent the warning that may show up depend on the value of the constants (#defines)
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4702)
|
||||
#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
|
||||
wil::task<github_version_result> get_github_version_info_async(const bool prerelease)
|
||||
{
|
||||
// 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)
|
||||
@@ -170,7 +166,7 @@ namespace updating
|
||||
return !ec ? std::optional{ installer_download_path } : std::nullopt;
|
||||
}
|
||||
|
||||
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version)
|
||||
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version)
|
||||
{
|
||||
auto installer_download_path = create_download_path();
|
||||
if (!installer_download_path)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <future>
|
||||
#include <filesystem>
|
||||
#include <variant>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
@@ -16,6 +15,7 @@
|
||||
#endif
|
||||
|
||||
#include <common/version/helper.h>
|
||||
#include <wil/coroutine.h>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
@@ -32,13 +32,15 @@ namespace updating
|
||||
};
|
||||
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
|
||||
|
||||
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);
|
||||
using github_version_result = std::expected<github_version_info, std::wstring>;
|
||||
#else
|
||||
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
|
||||
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::filesystem::path get_pending_updates_path();
|
||||
void cleanup_updates();
|
||||
|
||||
// non-localized
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <future>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Storage.Streams.h>
|
||||
#include <winrt/Windows.Web.Http.h>
|
||||
@@ -21,15 +22,15 @@ namespace http
|
||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||
}
|
||||
|
||||
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
|
||||
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> request(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 std::wstring(body);
|
||||
co_return body;
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url);
|
||||
(void)response.EnsureSuccessStatusCode();
|
||||
@@ -38,7 +39,7 @@ namespace http
|
||||
file_stream.Close();
|
||||
}
|
||||
|
||||
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
|
||||
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath, std::function<void(float)> progressUpdateCallback)
|
||||
{
|
||||
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<TreatWarningAsError>true</TreatWarningAsError>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<TreatWarningAsError>true</TreatWarningAsError>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<TreatWarningAsError>true</TreatWarningAsError>
|
||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -5140,7 +5140,7 @@ bool IsPenInverted( WPARAM wParam )
|
||||
// Captures the specified screen using the capture APIs
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
|
||||
wil::task<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,9 +5176,7 @@ std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDire
|
||||
framePool.Close();
|
||||
|
||||
auto texture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
||||
auto result = util::CopyD3DTexture(d3dDevice, texture, true);
|
||||
|
||||
co_return result;
|
||||
co_return util::CopyD3DTexture(d3dDevice, texture, true);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
@@ -5205,10 +5203,7 @@ winrt::com_ptr<ID3D11Texture2D>CaptureScreenshot(winrt::DirectXPixelFormat const
|
||||
|
||||
auto item = util::CreateCaptureItemForMonitor(hMon);
|
||||
|
||||
auto capture = CaptureScreenshotAsync(device, item, pixelFormat);
|
||||
capture.wait();
|
||||
|
||||
return capture.get();
|
||||
return CaptureScreenshotAsync(device, item, pixelFormat).get();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
// WIL
|
||||
#include <wil/com.h>
|
||||
#include <wil/resource.h>
|
||||
#include <wil/coroutine.h>
|
||||
|
||||
// DirectX
|
||||
#include <d3d11_4.h>
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
|
||||
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.Common\\Microsoft.CmdPal.Core.Common.csproj",
|
||||
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.ViewModels\\Microsoft.CmdPal.Core.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",
|
||||
|
||||
@@ -1,62 +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;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
{
|
||||
public static void InitializeLogger(ILogger implementation)
|
||||
{
|
||||
_logger = implementation;
|
||||
}
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogWarning(message, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogInfo(message, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogDebug(message, memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
|
||||
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogTrace(memberName, sourceFilePath, sourceLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
|
||||
void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
|
||||
void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
|
||||
void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
|
||||
void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
|
||||
void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Core.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,37 +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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
|
||||
public interface IExtensionService
|
||||
{
|
||||
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
|
||||
|
||||
// Task<IEnumerable<string>> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false);
|
||||
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(Microsoft.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
|
||||
|
||||
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
|
||||
|
||||
Task SignalStopExtensionsAsync();
|
||||
|
||||
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
|
||||
|
||||
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
|
||||
|
||||
void EnableExtension(string extensionUniqueId);
|
||||
|
||||
void DisableExtension(string extensionUniqueId);
|
||||
|
||||
///// <summary>
|
||||
///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
|
||||
///// being absent from the machine or in an unknown state.
|
||||
///// </summary>
|
||||
///// <param name="extension">The out of proc extension object</param>
|
||||
///// <returns>True only if the extension was disabled. False otherwise.</returns>
|
||||
// public Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a navigation request within Command Palette view models.
|
||||
/// </summary>
|
||||
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
|
||||
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
|
||||
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,72 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.ViewModels.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show details.
|
||||
/// </summary>
|
||||
public static string ShowDetailsCommand {
|
||||
get {
|
||||
return ResourceManager.GetString("ShowDetailsCommand", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ShowDetailsCommand" xml:space="preserve">
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
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,
|
||||
}
|
||||
167
src/modules/cmdpal/Microsoft.CmdPal.Common/CmdPalLogger.cs
Normal file
167
src/modules/cmdpal/Microsoft.CmdPal.Common/CmdPalLogger.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
// Adapter implementing Microsoft.Extensions.Logging.ILogger,
|
||||
// delegating to ManagedCommon.Logger.
|
||||
public sealed partial class CmdPalLogger : ILogger
|
||||
{
|
||||
private static readonly AsyncLocal<Stack<object>> _scopeStack = new();
|
||||
private readonly LogLevel _minLevel;
|
||||
|
||||
public string CurrentVersionLogDirectoryPath => Logger.CurrentVersionLogDirectoryPath;
|
||||
|
||||
public CmdPalLogger(LogLevel minLevel = LogLevel.Information)
|
||||
{
|
||||
_minLevel = minLevel;
|
||||
|
||||
// Ensure underlying logger initialized (idempotent if already done elsewhere).
|
||||
Logger.InitializeLogger("\\CmdPal\\Logs\\");
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _minLevel;
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
{
|
||||
var stack = _scopeStack.Value;
|
||||
if (stack is null)
|
||||
{
|
||||
stack = new Stack<object>();
|
||||
_scopeStack.Value = stack;
|
||||
}
|
||||
|
||||
stack.Push(state);
|
||||
return new Scope(stack);
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(formatter);
|
||||
var message = formatter(state, exception);
|
||||
if (string.IsNullOrEmpty(message) && exception is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scopeSuffix = BuildScopeSuffix();
|
||||
var eventPrefix = eventId.Id != 0 ? $"[{eventId.Id}/{eventId.Name}] " : string.Empty;
|
||||
var finalMessage = $"{eventPrefix}{message}{scopeSuffix}";
|
||||
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
// Existing stack: Trace logs an empty line; append message via Debug.
|
||||
Logger.LogTrace();
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
Logger.LogDebug(finalMessage);
|
||||
}
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
Logger.LogError(exception.Message, exception);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LogLevel.Debug:
|
||||
Logger.LogDebug(finalMessage);
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
Logger.LogError(exception.Message, exception);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LogLevel.Information:
|
||||
Logger.LogInfo(finalMessage);
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
Logger.LogError(exception.Message, exception);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LogLevel.Warning:
|
||||
Logger.LogWarning(finalMessage);
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
Logger.LogError(exception.Message, exception);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LogLevel.Error:
|
||||
case LogLevel.Critical:
|
||||
if (exception is not null)
|
||||
{
|
||||
Logger.LogError(finalMessage, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError(finalMessage);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LogLevel.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildScopeSuffix()
|
||||
{
|
||||
var stack = _scopeStack.Value;
|
||||
if (stack is null || stack.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Show most-recent first.
|
||||
return $" [Scopes: {string.Join(" => ", stack.ToArray())}]";
|
||||
}
|
||||
|
||||
private sealed partial class Scope : IDisposable
|
||||
{
|
||||
private readonly Stack<object> _stack;
|
||||
private bool _disposed;
|
||||
|
||||
public Scope(Stack<object> stack) => _stack = stack;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_stack.Count > 0)
|
||||
{
|
||||
_stack.Pop();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for building diagnostic and error messages.
|
||||
@@ -7,7 +7,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
public partial class ExtensionHostInstance
|
||||
{
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe boolean implementation using atomic operations
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using System.Threading;
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static partial class NativeEventWaiter
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class PathHelper
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// An async gate that ensures only one operation runs at a time.
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// An async gate that ensures only one value computation runs at a time.
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 Microsoft.Extensions.Logging;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.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, ILogger logger)
|
||||
{
|
||||
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)
|
||||
{
|
||||
Log_FailedToGetVersion(logger, 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, ILogger logger)
|
||||
{
|
||||
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)
|
||||
{
|
||||
Log_FailedToGetVersionFromExe(logger, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to get version from the package")]
|
||||
static partial void Log_FailedToGetVersion(ILogger logger, Exception ex);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to get version from the executable")]
|
||||
static partial void Log_FailedToGetVersionFromExe(ILogger logger, Exception ex);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known key chords used in the Command Palette and extensions.
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
164
src/modules/cmdpal/Microsoft.CmdPal.Common/PersistenceService.cs
Normal file
164
src/modules/cmdpal/Microsoft.CmdPal.Common/PersistenceService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
public partial class PersistenceService
|
||||
{
|
||||
private static bool TryParseJsonObject(string json, ILogger logger, [NotNullWhen(true)] out JsonObject? obj)
|
||||
{
|
||||
obj = null;
|
||||
try
|
||||
{
|
||||
obj = JsonNode.Parse(json) as JsonObject;
|
||||
return obj is not null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log_PersistenceParseFailure(logger, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSavedObject(string filePath, ILogger logger, [NotNullWhen(true)] out JsonObject? saved)
|
||||
{
|
||||
saved = null;
|
||||
|
||||
string oldContent;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
saved = new JsonObject();
|
||||
return true;
|
||||
}
|
||||
|
||||
oldContent = File.ReadAllText(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log_PersistenceReadFileFailure(logger, filePath, ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(oldContent))
|
||||
{
|
||||
Log_FileEmpty(logger, filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseJsonObject(oldContent, logger, out saved);
|
||||
}
|
||||
|
||||
public static T LoadObject<T>(string filePath, JsonTypeInfo<T> typeInfo, ILogger logger)
|
||||
where T : new()
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid file path before loading {typeof(T).Name}");
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Log_FileDoesntExist(logger, typeof(T).Name, filePath);
|
||||
return new T();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = File.ReadAllText(filePath);
|
||||
var loaded = JsonSerializer.Deserialize(jsonContent, typeInfo);
|
||||
return loaded ?? new T();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log_PersistenceReadFailure(logger, typeof(T).Name, filePath, ex);
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveObject<T>(
|
||||
T model,
|
||||
string filePath,
|
||||
JsonTypeInfo<T> typeInfo,
|
||||
JsonSerializerOptions optionsForWrite,
|
||||
Action<JsonObject>? beforeWriteMutation,
|
||||
Action<T>? afterWriteCallback,
|
||||
ILogger logger)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid file path before saving {typeof(T).Name}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(model, typeInfo);
|
||||
|
||||
if (!TryParseJsonObject(json, logger, out var newObj))
|
||||
{
|
||||
Log_SerializationError(logger, typeof(T).Name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryReadSavedObject(filePath, logger, out var savedObj))
|
||||
{
|
||||
savedObj = new JsonObject();
|
||||
}
|
||||
|
||||
foreach (var kvp in newObj)
|
||||
{
|
||||
savedObj[kvp.Key] = kvp.Value?.DeepClone();
|
||||
}
|
||||
|
||||
beforeWriteMutation?.Invoke(savedObj);
|
||||
|
||||
var serialized = savedObj.ToJsonString(optionsForWrite);
|
||||
File.WriteAllText(filePath, serialized);
|
||||
|
||||
afterWriteCallback?.Invoke(model);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log_PersistenceSaveFailure(logger, typeof(T).Name, filePath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SettingsJsonPath(string fileName)
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CommandPalette");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, fileName);
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to save {typeName} to '{filePath}'.")]
|
||||
static partial void Log_PersistenceSaveFailure(ILogger logger, string typeName, string filePath, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to read {typeName} from '{filePath}'.")]
|
||||
static partial void Log_PersistenceReadFailure(ILogger logger, string typeName, string filePath, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Failed to serialize {typeName} to JsonObject.")]
|
||||
static partial void Log_SerializationError(ILogger logger, string typeName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "The provided {typeName} file does not exist ({filePath})")]
|
||||
static partial void Log_FileDoesntExist(ILogger logger, string typeName, string filePath);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "The file at '{filePath}' is empty.")]
|
||||
static partial void Log_FileEmpty(ILogger logger, string filePath);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to read file at '{filePath}'.")]
|
||||
static partial void Log_PersistenceReadFileFailure(ILogger logger, string filePath, Exception exception);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to parse persisted JSON.")]
|
||||
static partial void Log_PersistenceParseFailure(ILogger logger, Exception exception);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
namespace Microsoft.CmdPal.Common.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
@@ -36,10 +36,10 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Common.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
@@ -51,7 +51,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
///
|
||||
///(While you’re at it, give the details below a quick skim — just to make sure there’s nothing personal you’d prefer not to share. It’s rare, but sometimes little surprises sneak in.).
|
||||
/// </summary>
|
||||
internal static string ErrorReport_Global_Preamble {
|
||||
public static string ErrorReport_Global_Preamble {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IApplicationInfoService providing application-wide information.
|
||||
/// </summary>
|
||||
public sealed partial 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 readonly ILogger _logger;
|
||||
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(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_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,
|
||||
ILogger logger)
|
||||
: this(logger)
|
||||
{
|
||||
_getLogDirectory = getLogDirectory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <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 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)
|
||||
{
|
||||
Log_FailedToDeterminePackagingFlavor(ex);
|
||||
return AppPackagingFlavor.Unpackaged;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DetermineElevationStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
return isElevated;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to determine packaging flavor")]
|
||||
partial void Log_FailedToDeterminePackagingFlavor(Exception ex);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public interface IRunHistoryService
|
||||
{
|
||||
@@ -2,20 +2,27 @@
|
||||
// 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);
|
||||
@@ -24,6 +31,9 @@ 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)
|
||||
@@ -38,18 +48,7 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
|
||||
Context: {context ?? "N/A"}
|
||||
|
||||
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}
|
||||
{applicationInfoSummarySanitized}
|
||||
|
||||
Stack Trace:
|
||||
{exception.StackTrace}
|
||||
@@ -66,31 +65,17 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
""";
|
||||
}
|
||||
|
||||
private static string GetElevationStatus()
|
||||
private string? GetAppInfoSafe()
|
||||
{
|
||||
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
|
||||
try
|
||||
{
|
||||
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
return isElevated ? "yes" : "no";
|
||||
return _appInfoService.GetApplicationInfoSummary();
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
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";
|
||||
// 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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
163
src/modules/cmdpal/Microsoft.CmdPal.Common/Text/StringFolder.cs
Normal file
163
src/modules/cmdpal/Microsoft.CmdPal.Common/Text/StringFolder.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -3,45 +3,44 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AliasManager : ObservableObject
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
// private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
|
||||
private readonly Dictionary<string, CommandAlias> _aliases;
|
||||
private Dictionary<string, CommandAlias> Aliases => _settingsService.CurrentSettings.Aliases;
|
||||
|
||||
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
|
||||
// TopLevelCommandManager tlcManager,
|
||||
public AliasManager(SettingsService settingsService)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_aliases = settings.Aliases;
|
||||
// _topLevelCommandManager = tlcManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
if (_aliases.Count == 0)
|
||||
if (Aliases.Count == 0)
|
||||
{
|
||||
PopulateDefaultAliases();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a);
|
||||
private void AddAlias(CommandAlias a) => Aliases.Add(a.SearchPrefix, a);
|
||||
|
||||
public bool CheckAlias(string searchText)
|
||||
{
|
||||
if (_aliases.TryGetValue(searchText, out var alias))
|
||||
if (Aliases.TryGetValue(searchText, out var alias))
|
||||
{
|
||||
try
|
||||
{
|
||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
|
||||
return true;
|
||||
}
|
||||
// var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
|
||||
// if (topLevelCommand is not null)
|
||||
// {
|
||||
// WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
|
||||
// WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
|
||||
// return true;
|
||||
// }
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -65,7 +64,7 @@ public partial class AliasManager : ObservableObject
|
||||
|
||||
public string? KeysFromId(string commandId)
|
||||
{
|
||||
return _aliases
|
||||
return Aliases
|
||||
.Where(kv => kv.Value.CommandId == commandId)
|
||||
.Select(kv => kv.Value.Alias)
|
||||
.FirstOrDefault();
|
||||
@@ -73,7 +72,7 @@ public partial class AliasManager : ObservableObject
|
||||
|
||||
public CommandAlias? AliasFromId(string commandId)
|
||||
{
|
||||
return _aliases
|
||||
return Aliases
|
||||
.Where(kv => kv.Value.CommandId == commandId)
|
||||
.Select(kv => kv.Value)
|
||||
.FirstOrDefault();
|
||||
@@ -89,7 +88,7 @@ public partial class AliasManager : ObservableObject
|
||||
|
||||
// If we already have _this exact alias_, do nothing
|
||||
if (newAlias is not null &&
|
||||
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||
Aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
|
||||
{
|
||||
if (existingAlias.CommandId == commandId)
|
||||
{
|
||||
@@ -98,7 +97,7 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
|
||||
List<CommandAlias> toRemove = [];
|
||||
foreach (var kv in _aliases)
|
||||
foreach (var kv in Aliases)
|
||||
{
|
||||
// Look for the old aliases for the command, and remove it
|
||||
if (kv.Value.CommandId == commandId)
|
||||
@@ -112,18 +111,18 @@ public partial class AliasManager : ObservableObject
|
||||
toRemove.Add(kv.Value);
|
||||
|
||||
// Remove alias from other TopLevelViewModels it may be assigned to
|
||||
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
topLevelCommand.AliasText = string.Empty;
|
||||
}
|
||||
// var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
|
||||
// if (topLevelCommand is not null)
|
||||
// {
|
||||
// topLevelCommand.AliasText = string.Empty;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var alias in toRemove)
|
||||
{
|
||||
// REMEMBER, SearchPrefix is what we use as keys
|
||||
_aliases.Remove(alias.SearchPrefix);
|
||||
Aliases.Remove(alias.SearchPrefix);
|
||||
}
|
||||
|
||||
if (newAlias is not null)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public abstract partial class AppExtensionHost : IExtensionHost
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private static readonly GlobalLogPageContext _globalLogPageContext = new();
|
||||
|
||||
private static ulong _hostingHwnd;
|
||||
@@ -27,6 +29,11 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
|
||||
public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd;
|
||||
|
||||
public AppExtensionHost(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void DebugLog(string message)
|
||||
{
|
||||
#if DEBUG
|
||||
@@ -60,7 +67,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
return Task.CompletedTask.AsAsyncAction();
|
||||
}
|
||||
|
||||
CoreLogger.LogDebug(message.Message);
|
||||
Log_Message(message.Message);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
@@ -158,6 +165,9 @@ public abstract partial class AppExtensionHost : IExtensionHost
|
||||
}
|
||||
|
||||
public abstract string? GetExtensionDisplayName();
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "{message}")]
|
||||
partial void Log_Message(string message);
|
||||
}
|
||||
|
||||
public interface IAppHostService
|
||||
@@ -2,25 +2,12 @@
|
||||
// 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 System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AppStateModel : ObservableObject
|
||||
{
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// STATE HERE
|
||||
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
|
||||
@@ -28,144 +15,4 @@ public partial class AppStateModel : ObservableObject
|
||||
public RecentCommandsManager RecentCommands { get; set; } = new();
|
||||
|
||||
public List<string> RunHistory { get; set; } = [];
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static AppStateModel()
|
||||
{
|
||||
FilePath = StateJsonPath();
|
||||
}
|
||||
|
||||
public static AppStateModel LoadState()
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
|
||||
}
|
||||
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
Debug.WriteLine("The provided settings file does not exist");
|
||||
return new();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
||||
|
||||
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||
|
||||
return loaded ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
public static void SaveState(AppStateModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
|
||||
|
||||
// validate JSON
|
||||
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
|
||||
{
|
||||
Logger.LogError("Failed to parse app state as a JsonObject.");
|
||||
return;
|
||||
}
|
||||
|
||||
// read previous settings
|
||||
if (!TryReadSavedState(out var savedSettings))
|
||||
{
|
||||
savedSettings = new JsonObject();
|
||||
}
|
||||
|
||||
// merge new settings into old ones
|
||||
foreach (var item in newSettings)
|
||||
{
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
model.StateChanged?.Invoke(model, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
|
||||
{
|
||||
savedSettings = null;
|
||||
|
||||
// read existing content from the file
|
||||
string oldContent;
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
oldContent = File.ReadAllText(FilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// file doesn't exist (might not have been created yet), so consider this a success
|
||||
// and return empty settings
|
||||
savedSettings = new JsonObject();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// detect empty file, just for sake of logging
|
||||
if (string.IsNullOrWhiteSpace(oldContent))
|
||||
{
|
||||
Logger.LogInfo($"App state file is empty: {FilePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// is it valid JSON?
|
||||
try
|
||||
{
|
||||
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
|
||||
return savedSettings != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string StateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the settings is just next to the exe
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class AppStateService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _filePath;
|
||||
private AppStateModel _appStateModel;
|
||||
|
||||
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
|
||||
|
||||
public AppStateModel CurrentSettings => _appStateModel;
|
||||
|
||||
public AppStateService(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_filePath = PersistenceService.SettingsJsonPath("state.json");
|
||||
_appStateModel = LoadState();
|
||||
}
|
||||
|
||||
private AppStateModel LoadState()
|
||||
{
|
||||
return PersistenceService.LoadObject<AppStateModel>(_filePath, JsonSerializationContext.Default.AppStateModel!, _logger);
|
||||
}
|
||||
|
||||
public void SaveSettings(AppStateModel model)
|
||||
{
|
||||
PersistenceService.SaveObject(
|
||||
model,
|
||||
_filePath,
|
||||
JsonSerializationContext.Default.AppStateModel,
|
||||
JsonSerializationContext.Default.Options,
|
||||
null,
|
||||
afterWriteCallback: m => FinalizeStateSave(m),
|
||||
_logger);
|
||||
}
|
||||
|
||||
private void FinalizeStateSave(AppStateModel model)
|
||||
{
|
||||
_appStateModel = model;
|
||||
StateChanged?.Invoke(model, null);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ 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
|
||||
@@ -85,7 +87,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
Color.FromArgb(255, 126, 115, 95), // #7e735f
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
@@ -94,22 +96,24 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
private SettingsModel Settings => _settingsService.CurrentSettings;
|
||||
|
||||
public ObservableCollection<Color> Swatches => WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_settings.Theme;
|
||||
get => (int)Settings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _settings.Theme;
|
||||
get => Settings.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Theme != value)
|
||||
if (Settings.Theme != value)
|
||||
{
|
||||
_settings.Theme = value;
|
||||
Settings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
@@ -119,19 +123,22 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settings.ColorizationMode;
|
||||
get => Settings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_settings.ColorizationMode != value)
|
||||
if (Settings.ColorizationMode != value)
|
||||
{
|
||||
_settings.ColorizationMode = value;
|
||||
Settings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsColorIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
OnPropertyChanged(nameof(IsResetButtonVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
@@ -147,18 +154,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_settings.ColorizationMode;
|
||||
get => (int)Settings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _settings.CustomThemeColor;
|
||||
get => Settings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_settings.CustomThemeColor != value)
|
||||
if (Settings.CustomThemeColor != value)
|
||||
{
|
||||
_settings.CustomThemeColor = value;
|
||||
Settings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
@@ -174,23 +181,36 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _settings.CustomThemeColorIntensity;
|
||||
get => Settings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_settings.CustomThemeColorIntensity = value;
|
||||
Settings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageTintIntensity
|
||||
{
|
||||
get => _settings.BackgroundImageTintIntensity;
|
||||
set
|
||||
{
|
||||
_settings.BackgroundImageTintIntensity = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _settings.BackgroundImagePath ?? string.Empty;
|
||||
get => Settings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImagePath != value)
|
||||
if (Settings.BackgroundImagePath != value)
|
||||
{
|
||||
_settings.BackgroundImagePath = value;
|
||||
Settings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
@@ -205,12 +225,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _settings.BackgroundImageOpacity;
|
||||
get => Settings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageOpacity != value)
|
||||
if (Settings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_settings.BackgroundImageOpacity = value;
|
||||
Settings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -219,12 +239,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _settings.BackgroundImageBrightness;
|
||||
get => Settings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBrightness != value)
|
||||
if (Settings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_settings.BackgroundImageBrightness = value;
|
||||
Settings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -233,12 +253,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _settings.BackgroundImageBlurAmount;
|
||||
get => Settings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageBlurAmount != value)
|
||||
if (Settings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_settings.BackgroundImageBlurAmount = value;
|
||||
Settings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
@@ -247,12 +267,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _settings.BackgroundImageFit;
|
||||
get => Settings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_settings.BackgroundImageFit != value)
|
||||
if (Settings.BackgroundImageFit != value)
|
||||
{
|
||||
_settings.BackgroundImageFit = value;
|
||||
Settings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
@@ -279,29 +299,131 @@ 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 IsCustomTintVisible => Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
|
||||
/// <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 IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
public bool IsBackgroundControlsVisible => Settings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||
public bool IsNoBackgroundVisible => Settings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
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 ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
public Color EffectiveThemeColor =>
|
||||
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
|
||||
? Colors.Transparent
|
||||
: 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);
|
||||
@@ -309,17 +431,19 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
!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;
|
||||
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
@@ -327,7 +451,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
|
||||
SettingsIsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
@@ -348,7 +472,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_settingsService.SaveSettings(Settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
@@ -357,6 +481,8 @@ 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));
|
||||
@@ -379,7 +505,28 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a navigation request within Command Palette view models.
|
||||
/// </summary>
|
||||
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
|
||||
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
|
||||
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public record AsyncListPageNavigationRequest(object? TargetViewModel, SettingsService SettingsService, ILogger Logger, CancellationToken NavigationToken)
|
||||
: AsyncNavigationRequest(TargetViewModel, NavigationToken);
|
||||
|
||||
public record AsyncContentPageNavigationRequest(object? TargetViewModel, object? ImageProvider, CancellationToken NavigationToken)
|
||||
: AsyncNavigationRequest(TargetViewModel, NavigationToken);
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -4,15 +4,18 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandBarViewModel : ObservableObject,
|
||||
IRecipient<UpdateCommandBarMessage>
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ICommandBarContext? SelectedItem
|
||||
{
|
||||
get => field;
|
||||
@@ -48,8 +51,9 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
[ObservableProperty]
|
||||
public partial PageViewModel? CurrentPage { get; set; }
|
||||
|
||||
public CommandBarViewModel()
|
||||
public CommandBarViewModel(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user