Compare commits

..

5 Commits

96 changed files with 463 additions and 4137 deletions

View File

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

View File

@@ -1527,7 +1527,6 @@ randi
RAquadrant
rasterization
Rasterize
rasterizing
RAWINPUTDEVICE
RAWINPUTHEADER
RAWMODE

View File

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

View File

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

View File

@@ -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 = get_github_version_info_async();
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 = download_new_version(std::get<new_version_download_info>(*new_version_info));
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");

View File

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

View File

@@ -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();
}

View File

@@ -83,18 +83,18 @@ namespace updating
#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)
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)
nonstd::expected<github_version_info, std::wstring> get_github_version_info_async(const bool prerelease)
#endif
{
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
if constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
{
#if USE_STD_EXPECTED
co_return std::unexpected(LOCAL_BUILD_ERROR);
return std::unexpected(LOCAL_BUILD_ERROR);
#else
co_return nonstd::make_unexpected(LOCAL_BUILD_ERROR);
return nonstd::make_unexpected(LOCAL_BUILD_ERROR);
#endif
}
@@ -107,7 +107,7 @@ namespace updating
if (prerelease)
{
const auto body = co_await client.request(Uri{ ALL_RELEASES_ENDPOINT });
const auto body = client.request(Uri{ ALL_RELEASES_ENDPOINT }).get();
for (const auto& json : json::JsonValue::Parse(body).GetArray())
{
auto potential_release_object = json.GetObjectW();
@@ -125,7 +125,7 @@ namespace updating
}
else
{
const auto body = co_await client.request(Uri{ LATEST_RELEASE_ENDPOINT });
const auto body = client.request(Uri{ LATEST_RELEASE_ENDPOINT }).get();
release_object = json::JsonValue::Parse(body).GetObjectW();
if (auto extracted_version = extract_version_from_release_object(release_object))
{
@@ -135,11 +135,11 @@ namespace updating
if (github_version <= current_version)
{
co_return version_up_to_date{};
return version_up_to_date{};
}
auto [installer_download_url, installer_filename] = extract_installer_asset_download_info(release_object);
co_return new_version_download_info{ extract_release_page_url(release_object),
return new_version_download_info{ extract_release_page_url(release_object),
std::move(github_version),
std::move(installer_download_url),
std::move(installer_filename) };
@@ -148,9 +148,9 @@ namespace updating
{
}
#if USE_STD_EXPECTED
co_return std::unexpected(NETWORK_ERROR);
return std::unexpected(NETWORK_ERROR);
#else
co_return nonstd::make_unexpected(NETWORK_ERROR);
return nonstd::make_unexpected(NETWORK_ERROR);
#endif
}
#pragma warning(pop)
@@ -170,12 +170,12 @@ 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)
std::optional<std::filesystem::path> download_new_version(const new_version_download_info& new_version)
{
auto installer_download_path = create_download_path();
if (!installer_download_path)
{
co_return std::nullopt;
return std::nullopt;
}
*installer_download_path /= new_version.installer_filename;
@@ -186,7 +186,7 @@ namespace updating
try
{
http::HttpClient client;
co_await client.download(new_version.installer_download_url, *installer_download_path);
client.download(new_version.installer_download_url, *installer_download_path).get();
download_success = true;
break;
}
@@ -195,7 +195,7 @@ namespace updating
// reattempt to download or do nothing
}
}
co_return download_success ? installer_download_path : std::nullopt;
return download_success ? installer_download_path : std::nullopt;
}
void cleanup_updates()

View File

@@ -2,7 +2,6 @@
#include <optional>
#include <string>
#include <future>
#include <filesystem>
#include <variant>
#include <winrt/Windows.Foundation.h>
@@ -32,12 +31,12 @@ 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::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);
std::expected<github_version_info, std::wstring> get_github_version_info_async(const bool prerelease = false);
#else
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
nonstd::expected<github_version_info, std::wstring> get_github_version_info_async(const bool prerelease = false);
#endif
void cleanup_updates();

View File

@@ -1,6 +1,6 @@
#pragma once
#include <future>
#include <functional>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Web.Http.h>
@@ -21,15 +21,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(const winrt::Windows::Foundation::Uri& url)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
auto body = co_await response.Content().ReadAsStringAsync();
co_return 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(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
@@ -38,7 +38,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(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
{
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
response.EnsureSuccessStatusCode();

View File

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

View File

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

View File

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

View File

@@ -5140,7 +5140,7 @@ bool IsPenInverted( WPARAM wParam )
// Captures the specified screen using the capture APIs
//
//----------------------------------------------------------------------------
std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
winrt::Windows::Foundation::IAsyncAction CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat, winrt::com_ptr<ID3D11Texture2D>& result)
{
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;
result = util::CopyD3DTexture(d3dDevice, texture, true);
}
//----------------------------------------------------------------------------
@@ -5205,10 +5203,9 @@ winrt::com_ptr<ID3D11Texture2D>CaptureScreenshot(winrt::DirectXPixelFormat const
auto item = util::CreateCaptureItemForMonitor(hMon);
auto capture = CaptureScreenshotAsync(device, item, pixelFormat);
capture.wait();
return capture.get();
winrt::com_ptr<ID3D11Texture2D> result;
CaptureScreenshotAsync(device, item, pixelFormat, result).get();
return result;
}

View File

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

View File

@@ -1,27 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Core.Common.Helpers;
/// <summary>
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
/// </summary>
public interface IPrecomputedListItem
{
/// <summary>
/// Gets the fuzzy matching target for the item's title.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the title.</returns>
FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher);
/// <summary>
/// Gets the fuzzy matching target for the item's subtitle.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the subtitle.</returns>
FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher);
}

View File

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

View File

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

View File

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

View File

@@ -1,49 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Services;
/// <summary>
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
/// </summary>
public interface IApplicationInfoService
{
/// <summary>
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
/// </summary>
string AppVersion { get; }
/// <summary>
/// Gets the packaging flavor of the application.
/// </summary>
AppPackagingFlavor PackagingFlavor { get; }
/// <summary>
/// Gets the directory path where application logs are stored.
/// </summary>
string LogDirectory { get; }
/// <summary>
/// Gets the directory path where application configuration files are stored.
/// </summary>
string ConfigDirectory { get; }
/// <summary>
/// Gets a value indicating whether the application is running with administrator privileges.
/// </summary>
bool IsElevated { get; }
/// <summary>
/// Gets a formatted summary of application information suitable for logging.
/// </summary>
/// <returns>A formatted string containing application information.</returns>
string GetApplicationInfoSummary();
/// <summary>
/// Sets the log directory delegate to be used for retrieving the log directory path.
/// This allows deferred initialization of the logger path.
/// </summary>
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
void SetLogDirectory(Func<string> getLogDirectory);
}

View File

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

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class BloomFilter : IBloomFilter
{
public ulong Compute(string input)
{
ulong bloom = 0;
foreach (var ch in input)
{
if (SymbolClassifier.Classify(ch) == SymbolKind.WordSeparator)
{
continue;
}
var h = (uint)ch * 0x45d9f3b;
bloom |= 1UL << (int)(h & 31);
bloom |= 1UL << (int)(((h >> 16) & 31) + 32);
if (bloom == ulong.MaxValue)
{
break;
}
}
return bloom;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MightContain(ulong candidateBloom, ulong queryBloom)
{
return (candidateBloom & queryBloom) == queryBloom;
}
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class FuzzyMatcherProvider : IFuzzyMatcherProvider
{
private readonly IBloomFilter _bloomCalculator = new BloomFilter();
private readonly IStringFolder _normalizer = new StringFolder();
private IPrecomputedFuzzyMatcher _current;
public FuzzyMatcherProvider(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
_current = CreateMatcher(core, pinyin);
}
public IPrecomputedFuzzyMatcher Current => Volatile.Read(ref _current);
public void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
Volatile.Write(ref _current, CreateMatcher(core, pinyin));
}
private IPrecomputedFuzzyMatcher CreateMatcher(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin)
{
return pinyin is null || !IsPinyinEnabled(pinyin)
? new PrecomputedFuzzyMatcher(core, _normalizer, _bloomCalculator)
: new PrecomputedFuzzyMatcherWithPinyin(core, pinyin, _normalizer, _bloomCalculator);
}
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o)
{
return o.Mode switch
{
PinyinMode.Off => false,
PinyinMode.On => true,
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
_ => false,
};
}
private static bool IsSimplifiedChineseUi()
{
var culture = CultureInfo.CurrentUICulture;
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public readonly struct FuzzyQuery
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly int EffectiveLength;
public readonly bool IsAllLowercaseAsciiOrNonLetter;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public readonly int SecondaryEffectiveLength;
public readonly bool SecondaryIsAllLowercaseAsciiOrNonLetter;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyQuery(
string original,
string folded,
ulong bloom,
int effectiveLength,
bool isAllLowercaseAsciiOrNonLetter,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0,
int secondaryEffectiveLength = 0,
bool secondaryIsAllLowercaseAsciiOrNonLetter = true)
{
Original = original;
Folded = folded;
Bloom = bloom;
EffectiveLength = effectiveLength;
IsAllLowercaseAsciiOrNonLetter = isAllLowercaseAsciiOrNonLetter;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
SecondaryEffectiveLength = secondaryEffectiveLength;
SecondaryIsAllLowercaseAsciiOrNonLetter = secondaryIsAllLowercaseAsciiOrNonLetter;
}
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public readonly struct FuzzyTarget
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public int SecondaryLength => SecondaryFolded?.Length ?? 0;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyTarget(
string original,
string folded,
ulong bloom,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0)
{
Original = original;
Folded = folded;
Bloom = bloom;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
}
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public struct FuzzyTargetCache
{
private string? _lastRaw;
private uint _schemaId;
private FuzzyTarget _target;
public FuzzyTarget GetOrUpdate(IPrecomputedFuzzyMatcher matcher, string? raw)
{
raw ??= string.Empty;
if (_schemaId == matcher.SchemaId && string.Equals(_lastRaw, raw, StringComparison.Ordinal))
{
return _target;
}
_target = matcher.PrecomputeTarget(raw);
_schemaId = matcher.SchemaId;
_lastRaw = raw;
return _target;
}
public void Invalidate()
{
_lastRaw = null;
_target = default;
_schemaId = 0;
}
}

View File

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

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public interface IFuzzyMatcherProvider
{
IPrecomputedFuzzyMatcher Current { get; }
void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null);
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public interface IPrecomputedFuzzyMatcher
{
uint SchemaId { get; }
FuzzyQuery PrecomputeQuery(string? input);
FuzzyTarget PrecomputeTarget(string? input);
int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target);
}

View File

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

View File

@@ -1,13 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PinyinFuzzyMatcherOptions
{
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
public bool RemoveApostrophesForQuery { get; init; } = true;
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public enum PinyinMode
{
Off = 0,
AutoSimplifiedChineseUi = 1,
On = 2,
}

View File

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

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcherOptions
{
public static PrecomputedFuzzyMatcherOptions Default { get; } = new();
/*
* Bonuses
*/
public int CharMatchBonus { get; init; } = 1;
public int SameCaseBonus { get; init; } = 1;
public int ConsecutiveMultiplier { get; init; } = 5;
public int CamelCaseBonus { get; init; } = 2;
public int StartOfWordBonus { get; init; } = 8;
public int PathSeparatorBonus { get; init; } = 5;
public int WordSeparatorBonus { get; init; } = 4;
public int SeparatorAlignmentBonus { get; init; } = 2;
public int ExactSeparatorBonus { get; init; } = 1;
/*
* Settings
*/
public bool RemoveDiacritics { get; init; } = true;
public bool SkipWordSeparators { get; init; } = true;
public bool IgnoreSameCaseBonusIfQueryIsAllLowercase { get; init; } = true;
}

View File

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

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Text;
internal enum SymbolKind : byte
{
Other = 0,
PathSeparator = 1,
WordSeparator = 2,
}

View File

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

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the type of system backdrop controller to use.
/// </summary>
public enum BackdropControllerKind
{
/// <summary>
/// Solid color with alpha transparency (TransparentTintBackdrop).
/// </summary>
Solid,
/// <summary>
/// Desktop Acrylic with default blur (DesktopAcrylicKind.Default).
/// </summary>
Acrylic,
/// <summary>
/// Desktop Acrylic with thinner blur (DesktopAcrylicKind.Thin).
/// </summary>
AcrylicThin,
/// <summary>
/// Mica effect (MicaKind.Base).
/// </summary>
Mica,
/// <summary>
/// Mica alternate/darker variant (MicaKind.BaseAlt).
/// </summary>
MicaAlt,
/// <summary>
/// Custom backdrop implementation.
/// </summary>
Custom,
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the visual backdrop style for the window.
/// </summary>
public enum BackdropStyle
{
/// <summary>
/// Standard desktop acrylic with blur effect.
/// </summary>
Acrylic,
/// <summary>
/// Solid color with alpha transparency (no blur).
/// </summary>
Clear,
/// <summary>
/// Mica effect that samples the desktop wallpaper.
/// </summary>
Mica,
/// <summary>
/// Thinner acrylic variant with more transparency.
/// </summary>
AcrylicThin,
/// <summary>
/// Mica alternate variant (darker).
/// </summary>
MicaAlt,
}

View File

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

View File

@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Central registry of backdrop style configurations.
/// </summary>
public static class BackdropStyles
{
private static readonly Dictionary<BackdropStyle, BackdropStyleConfig> Configs = new()
{
[BackdropStyle.Acrylic] = new()
{
ControllerKind = BackdropControllerKind.Acrylic,
BaseTintOpacity = 0.5f,
BaseLuminosityOpacity = 0.9f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.AcrylicThin] = new()
{
ControllerKind = BackdropControllerKind.AcrylicThin,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 0.85f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.Mica] = new()
{
ControllerKind = BackdropControllerKind.Mica,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.96f,
SupportsOpacity = false,
},
[BackdropStyle.MicaAlt] = new()
{
ControllerKind = BackdropControllerKind.MicaAlt,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.98f,
SupportsOpacity = false,
},
[BackdropStyle.Clear] = new()
{
ControllerKind = BackdropControllerKind.Solid,
BaseTintOpacity = 1.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
},
};
/// <summary>
/// Gets the configuration for the specified backdrop style.
/// </summary>
public static BackdropStyleConfig Get(BackdropStyle style) =>
Configs.TryGetValue(style, out var config) ? config : Configs[BackdropStyle.Acrylic];
/// <summary>
/// Gets all registered backdrop styles.
/// </summary>
public static IEnumerable<BackdropStyle> All => Configs.Keys;
}

View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the brush type to use for backdrop preview approximation.
/// </summary>
public enum PreviewBrushKind
{
/// <summary>
/// SolidColorBrush with computed alpha.
/// </summary>
Solid,
/// <summary>
/// AcrylicBrush with blur effect.
/// </summary>
Acrylic,
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);

View File

@@ -1,29 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.UI;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Parameters for configuring the window backdrop appearance.
/// </summary>
/// <param name="TintColor">The tint color applied to the backdrop.</param>
/// <param name="FallbackColor">The fallback color when backdrop effects are unavailable.</param>
/// <param name="EffectiveOpacity">
/// The effective opacity for the backdrop, pre-computed by the theme provider.
/// For Acrylic style: TintOpacity * BackdropOpacity.
/// For Clear style: BackdropOpacity (controls the solid color alpha).
/// </param>
/// <param name="EffectiveLuminosityOpacity">
/// The effective luminosity opacity for Acrylic backdrop, pre-computed by the theme provider.
/// Computed as LuminosityOpacity * BackdropOpacity.
/// </param>
/// <param name="Style">The backdrop style (Acrylic or Clear).</param>
public sealed record BackdropParameters(
Color TintColor,
Color FallbackColor,
float EffectiveOpacity,
float EffectiveLuminosityOpacity,
BackdropStyle Style = BackdropStyle.Acrylic);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,9 @@
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -23,19 +21,21 @@ public sealed partial class ContextMenu : UserControl,
IRecipient<UpdateCommandBarMessage>,
IRecipient<TryCommandKeybindingMessage>
{
public ContextMenuViewModel ViewModel { get; }
public ContextMenuViewModel ViewModel { get; } = new();
public ContextMenu()
{
this.InitializeComponent();
ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>());
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
if (ViewModel is not null)
{
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
}
public void Receive(OpenContextMenuMessage message)

View File

@@ -16,12 +16,12 @@ namespace Microsoft.CmdPal.UI.Controls;
/// </summary>
public partial class IconBox : ContentControl
{
private const double DefaultIconFontSize = 16.0;
private double _lastScale;
private ElementTheme _lastTheme;
private double _lastFontSize;
private const double DefaultIconFontSize = 16.0;
/// <summary>
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
/// </summary>
@@ -62,12 +62,6 @@ public partial class IconBox : ContentControl
{
Refresh();
}
#if DEBUG
if (_sourceRequested?.GetInvocationList().Length > 1)
{
Logger.LogWarning("There shouldn't be more than one handler for IconBox.SourceRequested");
}
#endif
}
remove => _sourceRequested -= value;
}
@@ -108,12 +102,9 @@ public partial class IconBox : ContentControl
if (Source is FontIconSource fontIcon)
{
fontIcon.FontSize = _lastFontSize;
UpdatePaddingForFontIcon();
}
}
private void UpdatePaddingForFontIcon() => Padding = new Thickness(Math.Round(_lastFontSize * -0.2));
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
if (_lastTheme == ActualTheme)
@@ -159,7 +150,10 @@ public partial class IconBox : ContentControl
private void Refresh()
{
UpdateSourceKey(this, SourceKey);
if (SourceKey is not null)
{
UpdateSourceKey(this, SourceKey);
}
}
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -176,10 +170,8 @@ public partial class IconBox : ContentControl
self.Padding = default;
break;
case FontIconSource fontIcon:
self.UpdateLastFontSize();
if (self.Content is IconSourceElement iconSourceElement)
{
fontIcon.FontSize = self._lastFontSize;
iconSourceElement.IconSource = fontIcon;
}
else
@@ -198,7 +190,7 @@ public partial class IconBox : ContentControl
self.Content = elem;
}
self.UpdatePaddingForFontIcon();
self.Padding = new Thickness(Math.Round(self._lastFontSize * -0.2));
break;
case BitmapIconSource bitmapIcon:
@@ -214,12 +206,10 @@ public partial class IconBox : ContentControl
self.Padding = default;
break;
case IconSource source:
self.Content = source.CreateIconElement();
self.Padding = default;
break;
default:
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
}
@@ -243,10 +233,10 @@ public partial class IconBox : ContentControl
return;
}
RequestIconFromSource(iconBox, sourceKey);
Callback(iconBox, sourceKey);
}
private static async void RequestIconFromSource(IconBox iconBox, object? sourceKey)
private static async void Callback(IconBox iconBox, object? sourceKey)
{
try
{
@@ -266,12 +256,17 @@ public partial class IconBox : ContentControl
// list virtualization situation, it's very possible we
// may have already been set to a new icon before we
// even got back from the await.
if (!ReferenceEquals(sourceKey, iconBox.SourceKey))
if (eventArgs.Key != sourceKey)
{
// If the requested icon has changed, then just bail
return;
}
if (eventArgs.Value == iconBox.Source)
{
return;
}
iconBox.Source = eventArgs.Value;
}
catch (Exception ex)

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.Common.Services.Reports;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -18,20 +17,19 @@ namespace Microsoft.CmdPal.UI.Helpers;
/// </summary>
internal sealed partial class GlobalErrorHandler : IDisposable
{
private ErrorReportBuilder? _errorReportBuilder;
private readonly ErrorReportBuilder _errorReportBuilder = new();
private Options? _options;
private App? _app;
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
internal void Register(App app, Options options, IApplicationInfoService? appInfoService = null)
internal void Register(App app, Options options)
{
ArgumentNullException.ThrowIfNull(app);
ArgumentNullException.ThrowIfNull(options);
_options = options;
_app = app;
_errorReportBuilder = new ErrorReportBuilder(appInfoService);
_app = app;
_app.UnhandledException += App_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -70,7 +68,7 @@ internal sealed partial class GlobalErrorHandler : IDisposable
if (context == Context.MainThreadException)
{
var report = _errorReportBuilder!.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
var report = _errorReportBuilder.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true);
StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);

View File

@@ -219,6 +219,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
iconSize = DefaultIconSize;
}
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
return IconPathConverter.IconSourceMUX(iconString, false, fontFamily, iconSize);
}
}

View File

@@ -22,7 +22,7 @@
VerticalAlignment="Stretch"
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
ImageOpacity="{x:Bind ViewModel.EffectiveImageOpacity, Mode=OneWay}"
ImageOpacity="{x:Bind ViewModel.BackgroundImageOpacity, Mode=OneWay}"
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
IsHitTestVisible="False"
@@ -31,6 +31,6 @@
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
<pages:ShellPage HostWindow="{x:Bind}" />
<pages:ShellPage />
</Grid>
</winuiex:WindowEx>

View File

@@ -31,7 +31,6 @@ using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.UI;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
@@ -54,8 +53,7 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<ErrorOccurredMessage>,
IRecipient<DragStartedMessage>,
IRecipient<DragCompletedMessage>,
IDisposable,
IHostWindow
IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
@@ -82,9 +80,7 @@ public sealed partial class MainWindow : WindowEx,
private int _sessionErrorCount;
private DesktopAcrylicController? _acrylicController;
private MicaController? _micaController;
private SystemBackdropConfiguration? _configurationSource;
private bool _isUpdatingBackdrop;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
private WindowPosition _currentWindowPosition = new();
@@ -93,8 +89,6 @@ public sealed partial class MainWindow : WindowEx,
private MainWindowViewModel ViewModel { get; }
public bool IsVisibleToUser { get; private set; } = true;
public MainWindow()
{
InitializeComponent();
@@ -115,7 +109,7 @@ public sealed partial class MainWindow : WindowEx,
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
}
InitializeBackdropSupport();
SetAcrylic();
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
@@ -164,7 +158,7 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
@@ -191,7 +185,7 @@ public sealed partial class MainWindow : WindowEx,
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
{
UpdateBackdrop();
UpdateAcrylic();
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
@@ -286,170 +280,48 @@ public sealed partial class MainWindow : WindowEx,
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
}
private void InitializeBackdropSupport()
private void SetAcrylic()
{
if (DesktopAcrylicController.IsSupported() || MicaController.IsSupported())
if (DesktopAcrylicController.IsSupported())
{
// Hooking up the policy object.
_configurationSource = new SystemBackdropConfiguration
{
// Initial configuration state.
IsInputActive = true,
};
UpdateAcrylic();
}
}
private void UpdateBackdrop()
private void UpdateAcrylic()
{
// Prevent re-entrance when backdrop changes trigger ActualThemeChanged
if (_isUpdatingBackdrop)
{
return;
}
_isUpdatingBackdrop = true;
var backdrop = _themeService.Current.BackdropParameters;
var isImageMode = ViewModel.ShowBackgroundImage;
var config = BackdropStyles.Get(backdrop.Style);
try
{
switch (config.ControllerKind)
if (_acrylicController != null)
{
case BackdropControllerKind.Solid:
CleanupBackdropControllers();
var tintColor = Color.FromArgb(
(byte)(backdrop.EffectiveOpacity * 255),
backdrop.TintColor.R,
backdrop.TintColor.G,
backdrop.TintColor.B);
SetupTransparentBackdrop(tintColor);
break;
case BackdropControllerKind.Mica:
case BackdropControllerKind.MicaAlt:
SetupMica(backdrop, isImageMode, config.ControllerKind);
break;
case BackdropControllerKind.Acrylic:
case BackdropControllerKind.AcrylicThin:
default:
SetupDesktopAcrylic(backdrop, isImageMode, config.ControllerKind);
break;
_acrylicController.RemoveAllSystemBackdropTargets();
_acrylicController.Dispose();
}
var backdrop = _themeService.Current.BackdropParameters;
_acrylicController = new DesktopAcrylicController
{
TintColor = backdrop.TintColor,
TintOpacity = backdrop.TintOpacity,
FallbackColor = backdrop.FallbackColor,
LuminosityOpacity = backdrop.LuminosityOpacity,
};
// Enable the system backdrop.
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
}
catch (Exception ex)
{
Logger.LogError("Failed to update backdrop", ex);
}
finally
{
_isUpdatingBackdrop = false;
}
}
private void SetupTransparentBackdrop(Color tintColor)
{
if (SystemBackdrop is TransparentTintBackdrop existingBackdrop)
{
existingBackdrop.TintColor = tintColor;
}
else
{
SystemBackdrop = new TransparentTintBackdrop { TintColor = tintColor };
}
}
private void CleanupBackdropControllers()
{
if (_acrylicController is not null)
{
_acrylicController.RemoveAllSystemBackdropTargets();
_acrylicController.Dispose();
_acrylicController = null;
}
if (_micaController is not null)
{
_micaController.RemoveAllSystemBackdropTargets();
_micaController.Dispose();
_micaController = null;
}
}
private void SetupDesktopAcrylic(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
{
CleanupBackdropControllers();
// Fall back to solid color if acrylic not supported
if (_configurationSource is null || !DesktopAcrylicController.IsSupported())
{
SetupTransparentBackdrop(backdrop.FallbackColor);
return;
}
// DesktopAcrylicController and SystemBackdrop can't be active simultaneously
SystemBackdrop = null;
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
var effectiveTintOpacity = isImageMode
? 0.0f
: backdrop.EffectiveOpacity;
_acrylicController = new DesktopAcrylicController
{
Kind = kind == BackdropControllerKind.AcrylicThin
? DesktopAcrylicKind.Thin
: DesktopAcrylicKind.Default,
TintColor = backdrop.TintColor,
TintOpacity = effectiveTintOpacity,
FallbackColor = backdrop.FallbackColor,
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
};
// Requires "using WinRT;" for Window.As<>()
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
}
private void SetupMica(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
{
CleanupBackdropControllers();
// Fall back to solid color if Mica not supported
if (_configurationSource is null || !MicaController.IsSupported())
{
SetupTransparentBackdrop(backdrop.FallbackColor);
return;
}
// MicaController and SystemBackdrop can't be active simultaneously
SystemBackdrop = null;
_configurationSource.Theme = _themeService.Current.Theme == ElementTheme.Dark
? SystemBackdropTheme.Dark
: SystemBackdropTheme.Light;
var hasColorization = _themeService.Current.HasColorization || isImageMode;
_micaController = new MicaController
{
Kind = kind == BackdropControllerKind.MicaAlt
? MicaKind.BaseAlt
: MicaKind.Base,
};
// Only set tint properties when colorization is active
// Otherwise let system handle light/dark theme defaults automatically
if (hasColorization)
{
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
_micaController.TintColor = backdrop.TintColor;
_micaController.TintOpacity = isImageMode ? 0.0f : backdrop.EffectiveOpacity;
_micaController.FallbackColor = backdrop.FallbackColor;
_micaController.LuminosityOpacity = backdrop.EffectiveLuminosityOpacity;
}
_micaController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
_micaController.SetSystemBackdropConfiguration(_configurationSource);
}
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
@@ -703,14 +575,17 @@ public sealed partial class MainWindow : WindowEx,
{
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
}
else
{
IsVisibleToUser = false;
}
wasCloaked = hr.Succeeded;
}
if (wasCloaked)
{
// Because we're only cloaking the window, bury it at the bottom in case something can
// see it - e.g. some accessibility helper (note: this also removes the top-most status).
PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
return wasCloaked;
}
@@ -720,7 +595,6 @@ public sealed partial class MainWindow : WindowEx,
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
IsVisibleToUser = true;
}
}
@@ -763,8 +637,12 @@ public sealed partial class MainWindow : WindowEx,
private void DisposeAcrylic()
{
CleanupBackdropControllers();
_configurationSource = null!;
if (_acrylicController is not null)
{
_acrylicController.Dispose();
_acrylicController = null!;
_configurationSource = null!;
}
}
// Updates our window s.t. the top of the window is draggable.

View File

@@ -13,7 +13,6 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CommandPalette.Extensions;
@@ -73,8 +72,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public event PropertyChangedEventHandler? PropertyChanged;
public IHostWindow? HostWindow { get; set; }
public ShellPage()
{
this.InitializeComponent();
@@ -438,7 +435,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (!RootFrame.CanGoBack)
{
ViewModel.GoHome(withAnimation, focusSearch);
ViewModel.GoHome();
}
if (focusSearch)
@@ -534,11 +531,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (shouldSearchBoxBeVisible || page is not ContentPage)
{
if (HostWindow?.IsVisibleToUser != true)
{
return;
}
ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible;
SearchBox.Focus(FocusState.Programmatic);
SearchBox.SelectSearch();
@@ -555,11 +547,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
try
{
if (HostWindow?.IsVisibleToUser != true)
{
return;
}
await page.DispatcherQueue.EnqueueAsync(
async () =>
{
@@ -569,11 +556,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
token.ThrowIfCancellationRequested();
if (HostWindow?.IsVisibleToUser != true)
{
break;
}
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
{
var set = frameworkElement.Focus(FocusState.Programmatic);

View File

@@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
@@ -24,13 +23,13 @@ internal sealed class PowerToysRootPageService : IRootPageService
private IExtensionWrapper? _activeExtension;
private Lazy<MainListPage> _mainListPage;
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel, IFuzzyMatcherProvider fuzzyMatcherProvider)
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
{
_tlcManager = topLevelCommandManager;
_mainListPage = new Lazy<MainListPage>(() =>
{
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel, fuzzyMatcherProvider);
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel);
});
}

View File

@@ -4,7 +4,6 @@
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
@@ -66,25 +65,6 @@ internal sealed class Program
}
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
// Log application startup information
try
{
var appInfoService = new ApplicationInfoService(() => Logger.CurrentVersionLogDirectoryPath);
var startupMessage = $"""
============================================================
Hello World! Command Palette is starting.
{appInfoService.GetApplicationInfoSummary()}
============================================================
""";
Logger.LogInfo(startupMessage);
}
catch (Exception ex)
{
Logger.LogError("Failed to log application startup information", ex);
}
PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted());
WinRT.ComWrappersSupport.InitializeComWrappers();

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.WinUI.Helpers;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.UI.Xaml;
using Windows.UI;
@@ -35,7 +34,7 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
_uiSettings = uiSettings;
}
public BackdropParameters GetBackdropParameters(ThemeContext context)
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
{
var isLight = context.Theme == ElementTheme.Light ||
(context.Theme == ElementTheme.Default &&
@@ -54,26 +53,7 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
var transparencyMode = context.BackdropStyle ?? BackdropStyle.Acrylic;
var config = BackdropStyles.Get(transparencyMode);
// For colorful theme, boost tint opacity to show color better through blur
// But not for styles with fixed opacity (Mica) - they handle their own opacity
var baseTintOpacity = config.ControllerKind == BackdropControllerKind.Solid || !config.SupportsOpacity
? (float?)null // Use default
: Math.Max(config.BaseTintOpacity, 0.8f);
var effectiveOpacity = config.ComputeEffectiveOpacity(context.BackdropOpacity, baseTintOpacity);
var effectiveLuminosityOpacity = config.SupportsOpacity
? config.BaseLuminosityOpacity * context.BackdropOpacity
: config.BaseLuminosityOpacity;
return new BackdropParameters(
TintColor: effectiveBgColor,
FallbackColor: effectiveBgColor,
EffectiveOpacity: effectiveOpacity,
EffectiveLuminosityOpacity: effectiveLuminosityOpacity,
Style: transparencyMode);
return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f);
}
private static class ColorBlender

View File

@@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.Services;
/// <summary>
/// Represents abstract host window functionality.
/// </summary>
public interface IHostWindow
{
/// <summary>
/// Gets a value indicating whether the window is visible to the user, taking account not only window visibility but also cloaking.
/// </summary>
bool IsVisibleToUser { get; }
}

View File

@@ -8,14 +8,14 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
namespace Microsoft.CmdPal.UI.Services;
/// <summary>
/// Provides theme identification, resource path resolution, and creation of backdrop
/// parameters based on the current <see cref="ThemeContext"/>.
/// Provides theme identification, resource path resolution, and creation of acrylic
/// backdrop parameters based on the current <see cref="ThemeContext"/>.
/// </summary>
/// <remarks>
/// Implementations should expose a stable <see cref="ThemeKey"/> and a valid XAML resource
/// dictionary path via <see cref="ResourcePath"/>. The
/// <see cref="GetBackdropParameters(ThemeContext)"/> method computes
/// <see cref="BackdropParameters"/> using the supplied theme context.
/// <see cref="GetAcrylicBackdrop(ThemeContext)"/> method computes
/// <see cref="AcrylicBackdropParameters"/> using the supplied theme context.
/// </remarks>
internal interface IThemeProvider
{
@@ -30,9 +30,9 @@ internal interface IThemeProvider
string ResourcePath { get; }
/// <summary>
/// Creates backdrop parameters based on the provided theme context.
/// Creates acrylic backdrop parameters based on the provided theme context.
/// </summary>
/// <param name="context">The current theme context, including theme, tint, transparency mode, and optional background details.</param>
/// <returns>The computed <see cref="BackdropParameters"/> for the backdrop.</returns>
BackdropParameters GetBackdropParameters(ThemeContext context);
/// <param name="context">The current theme context, including theme, tint, and optional background details.</param>
/// <returns>The computed <see cref="AcrylicBackdropParameters"/> for the backdrop.</returns>
AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context);
}

View File

@@ -2,7 +2,6 @@
// 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.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.UI.Xaml;
using Windows.UI;
@@ -29,28 +28,16 @@ internal sealed class NormalThemeProvider : IThemeProvider
public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
public BackdropParameters GetBackdropParameters(ThemeContext context)
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
{
var isLight = context.Theme == ElementTheme.Light ||
(context.Theme == ElementTheme.Default &&
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
var backdropStyle = context.BackdropStyle ?? BackdropStyle.Acrylic;
var config = BackdropStyles.Get(backdropStyle);
// Apply light/dark theme adjustment to luminosity
var baseLuminosityOpacity = isLight
? config.BaseLuminosityOpacity
: Math.Min(config.BaseLuminosityOpacity + 0.06f, 1.0f);
var effectiveOpacity = config.ComputeEffectiveOpacity(context.BackdropOpacity);
var effectiveLuminosityOpacity = baseLuminosityOpacity * context.BackdropOpacity;
return new BackdropParameters(
return new AcrylicBackdropParameters(
TintColor: isLight ? LightBaseColor : DarkBaseColor,
FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
EffectiveOpacity: effectiveOpacity,
EffectiveLuminosityOpacity: effectiveLuminosityOpacity,
Style: backdropStyle);
TintOpacity: 0.5f,
LuminosityOpacity: isLight ? 0.9f : 0.96f);
}
}

View File

@@ -2,16 +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 Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Microsoft.CmdPal.UI.Services;
/// <summary>
/// Input parameters for theme computation, passed to theme providers.
/// </summary>
internal sealed record ThemeContext
{
public ElementTheme Theme { get; init; }
@@ -25,8 +21,4 @@ internal sealed record ThemeContext
public double BackgroundImageOpacity { get; init; }
public int? ColorIntensity { get; init; }
public BackdropStyle? BackdropStyle { get; init; }
public float BackdropOpacity { get; init; } = 1.0f;
}

View File

@@ -72,13 +72,10 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
}
// provider selection
var themeColorIntensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
var imageTintIntensity = Math.Clamp(_settings.BackgroundImageTintIntensity, 0, 100);
var effectiveColorIntensity = _settings.ColorizationMode == ColorizationMode.Image
? imageTintIntensity
: themeColorIntensity;
IThemeProvider provider = UseColorfulProvider(effectiveColorIntensity) ? _colorfulThemeProvider : _normalThemeProvider;
var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
? _colorfulThemeProvider
: _normalThemeProvider;
// Calculate values
var tint = _settings.ColorizationMode switch
@@ -99,39 +96,32 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
};
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
// create input and offload to actual theme provider
// create context and offload to actual theme provider
var context = new ThemeContext
{
Tint = tint,
ColorIntensity = effectiveColorIntensity,
ColorIntensity = intensity,
Theme = effectiveTheme,
BackgroundImageSource = imageSource,
BackgroundImageStretch = stretch,
BackgroundImageOpacity = opacity,
BackdropStyle = _settings.BackdropStyle,
BackdropOpacity = Math.Clamp(_settings.BackdropOpacity, 0, 100) / 100f,
};
var backdrop = provider.GetBackdropParameters(context);
var backdrop = provider.GetAcrylicBackdrop(context);
var blur = _settings.BackgroundImageBlurAmount;
var brightness = _settings.BackgroundImageBrightness;
// Create public snapshot (no provider!)
var hasColorization = effectiveColorIntensity > 0
&& _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
var snapshot = new ThemeSnapshot
{
Tint = tint,
TintIntensity = effectiveColorIntensity / 100f,
TintIntensity = intensity / 100f,
Theme = effectiveTheme,
BackgroundImageSource = imageSource,
BackgroundImageStretch = stretch,
BackgroundImageOpacity = opacity,
BackdropParameters = backdrop,
BackdropOpacity = context.BackdropOpacity,
BlurAmount = blur,
BackgroundBrightness = brightness / 100f,
HasColorization = hasColorization,
};
// Bundle with provider for internal use
@@ -148,12 +138,6 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
}
private bool UseColorfulProvider(int effectiveColorIntensity)
{
return _settings.ColorizationMode == ColorizationMode.Image
|| (effectiveColorIntensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor);
}
private static BitmapImage? LoadImageSafe(string? path)
{
if (string.IsNullOrWhiteSpace(path))
@@ -211,15 +195,13 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
{
Tint = Colors.Transparent,
Theme = ElementTheme.Light,
BackdropParameters = new BackdropParameters(Colors.Black, Colors.Black, EffectiveOpacity: 0.5f, EffectiveLuminosityOpacity: 0.5f),
BackdropOpacity = 1.0f,
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
BackgroundImageOpacity = 1,
BackgroundImageSource = null,
BackgroundImageStretch = Stretch.Fill,
BlurAmount = 0,
TintIntensity = 1.0f,
BackgroundBrightness = 0,
HasColorization = false,
},
Provider = _normalThemeProvider,
};

View File

@@ -22,50 +22,18 @@
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel
Margin="0,0,0,16"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="16">
<ptControls:ScreenPreview>
<ptControls:CommandPalettePreview
PreviewBackdropStyle="{x:Bind ViewModel.Appearance.EffectiveBackdropStyle, Mode=OneWay}"
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
PreviewBackgroundImageOpacity="{x:Bind ViewModel.Appearance.EffectiveImageOpacity, Mode=OneWay}"
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.EffectiveTintIntensity, Mode=OneWay}"
PreviewEffectiveOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.EffectiveOpacity, Mode=OneWay}"
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
</ptControls:ScreenPreview>
<StackPanel VerticalAlignment="Bottom" Spacing="8">
<Button
x:Uid="Settings_AppearancePage_OpenCommandPaletteButton"
MinWidth="200"
HorizontalContentAlignment="Left"
Click="OpenCommandPalette_Click"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE8A7;" />
<TextBlock x:Uid="Settings_AppearancePage_OpenCommandPaletteButton_Text" />
</StackPanel>
</Button>
<Button
x:Uid="Settings_AppearancePage_ResetAppearanceButton"
MinWidth="200"
HorizontalContentAlignment="Left"
Command="{x:Bind ViewModel.Appearance.ResetAppearanceSettingsCommand}"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE72C;" />
<TextBlock x:Uid="Settings_AppearancePage_ResetAppearanceButton_Text" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
<ptControls:ScreenPreview Margin="0,0,0,16" HorizontalAlignment="Left">
<ptControls:CommandPalettePreview
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=OneWay}"
PreviewBackgroundOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintOpacity, Mode=OneWay}"
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
</ptControls:ScreenPreview>
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE793;}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
@@ -94,67 +62,19 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="Settings_GeneralPage_BackdropStyle_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}"
IsExpanded="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Acrylic" />
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Transparent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Mica" />
<!-- Hidden: preview not working well, kept to preserve index mapping -->
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_AcrylicThin" Visibility="Collapsed" />
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_MicaAlt" />
</ComboBox>
<controls:SettingsExpander.Items>
<!-- Mica description (no opacity control) -->
<controls:SettingsCard
x:Uid="Settings_GeneralPage_MicaBackdrop_SettingsCard"
HorizontalContentAlignment="Stretch"
ContentAlignment="Vertical"
Visibility="{x:Bind ViewModel.Appearance.IsMicaBackdropDescriptionVisible, Mode=OneWay}">
<TextBlock
x:Uid="Settings_GeneralPage_MicaBackdrop_DescriptionTextBlock"
Margin="24"
HorizontalAlignment="Stretch"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
HorizontalTextAlignment="Center"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
</controls:SettingsCard>
<!-- Opacity slider (for non-Mica styles) -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackdropOpacity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="0"
StepFrequency="1"
Value="{x:Bind ViewModel.Appearance.BackdropOpacity, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsExpander
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsEnabled="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}"
IsExpanded="{x:Bind ViewModel.Appearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
<Grid>
<ComboBox
x:Uid="Settings_GeneralPage_ColorizationMode"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
</ComboBox>
<TextBlock
x:Uid="Settings_GeneralPage_Background_NotAvailable"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundNotAvailableVisible, Mode=OneWay}" />
</Grid>
<ComboBox
x:Uid="Settings_GeneralPage_ColorizationMode"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
</ComboBox>
<controls:SettingsExpander.Items>
<!-- none -->
<controls:SettingsCard
@@ -235,7 +155,7 @@
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
SelectedColor="{x:Bind ViewModel.Appearance.ThemeColor, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsColorIntensityVisible, Mode=OneWay}">
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
@@ -243,17 +163,9 @@
StepFrequency="1"
Value="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_ImageTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsImageTintIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
Maximum="100"
Minimum="0"
StepFrequency="1"
Value="{x:Bind ViewModel.Appearance.BackgroundImageTintIntensity, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Reset appearance properties -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsResetButtonVisible, Mode=OneWay}">
<!-- Reset background image properties -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
</StackPanel>

View File

@@ -3,13 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI;
@@ -17,7 +12,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.Windows.Storage.Pickers;
using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.UI.Settings;
@@ -92,9 +86,4 @@ public sealed partial class AppearancePage : Page
}
});
}
private void OpenCommandPalette_Click(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(string.Empty, HWND.Null));
}
}

View File

@@ -2,13 +2,15 @@
// 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.Globalization;
using Microsoft.CmdPal.Core.Common.Services;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.UI.Settings;
@@ -17,7 +19,6 @@ public sealed partial class GeneralPage : Page
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SettingsViewModel? viewModel;
private readonly IApplicationInfoService _appInfoService;
public GeneralPage()
{
@@ -26,7 +27,6 @@ public sealed partial class GeneralPage : Page
var settings = App.Current.Services.GetService<SettingsModel>()!;
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
var themeService = App.Current.Services.GetService<IThemeService>()!;
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
}
@@ -35,8 +35,55 @@ public sealed partial class GeneralPage : Page
get
{
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
var version = _appInfoService.AppVersion;
if (!TryGetPackagedVersion(out var version) && !TryGetAssemblyVersion(out version))
{
version = "?";
}
return string.Format(CultureInfo.CurrentCulture, versionNo, version);
}
}
private static bool TryGetPackagedVersion(out string version)
{
version = string.Empty;
try
{
// Package.Current throws InvalidOperationException if the app is not packaged
var v = Package.Current.Id.Version;
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
return true;
}
catch (InvalidOperationException)
{
return false;
}
catch (Exception ex)
{
Logger.LogError("Failed to get version from the package", ex);
return false;
}
}
private static bool TryGetAssemblyVersion(out string version)
{
version = string.Empty;
try
{
var processPath = Environment.ProcessPath;
if (string.IsNullOrEmpty(processPath))
{
return false;
}
var info = FileVersionInfo.GetVersionInfo(processPath);
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
return true;
}
catch (Exception ex)
{
Logger.LogError("Failed to get version from the executable", ex);
return false;
}
}
}

View File

@@ -3,8 +3,7 @@
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml;
using Windows.System;
using Page = Microsoft.UI.Xaml.Controls.Page;
@@ -16,13 +15,9 @@ namespace Microsoft.CmdPal.UI.Settings;
/// </summary>
public sealed partial class InternalPage : Page
{
private readonly IApplicationInfoService _appInfoService;
public InternalPage()
{
InitializeComponent();
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
}
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
@@ -51,7 +46,7 @@ public sealed partial class InternalPage : Page
{
try
{
var logFolderPath = _appInfoService.LogDirectory;
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
if (Directory.Exists(logFolderPath))
{
await Launcher.LaunchFolderPathAsync(logFolderPath);
@@ -83,7 +78,7 @@ public sealed partial class InternalPage : Page
{
try
{
var directory = _appInfoService.ConfigDirectory;
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
if (Directory.Exists(directory))
{
await Launcher.LaunchFolderPathAsync(directory);

View File

@@ -577,9 +577,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
<value>Color intensity</value>
</data>
<data name="Settings_GeneralPage_ImageTintIntensity_SettingsCard.Header" xml:space="preserve">
<value>Color intensity</value>
</data>
<data name="OptionalColorPickerButton_UnsetTextBlock.Text" xml:space="preserve">
<value>Choose color</value>
</data>
@@ -671,7 +668,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Background image</value>
</data>
<data name="Settings_GeneralPage_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
<value>No additional settings are available.</value>
<value>No settings</value>
</data>
<data name="Settings_GeneralPage_Background_SettingsExpander.Header" xml:space="preserve">
<value>Background</value>
@@ -679,9 +676,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_Background_SettingsExpander.Description" xml:space="preserve">
<value>Choose a custom background color or image</value>
</data>
<data name="Settings_GeneralPage_Background_NotAvailable.Text" xml:space="preserve">
<value>Not available with Mica</value>
</data>
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
<value>System accent color</value>
</data>
@@ -698,7 +692,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Restore defaults</value>
</data>
<data name="Settings_GeneralPage_Background_ResetImagePropertiesButton.Content" xml:space="preserve">
<value>Reset image settings</value>
<value>Reset</value>
</data>
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1.Text" xml:space="preserve">
<value>Change the system accent in Windows Settings:</value>
@@ -733,48 +727,4 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
<value>Version {0}</value>
</data>
<data name="Settings_GeneralPage_BackdropOpacity_SettingsCard.Header" xml:space="preserve">
<value>Opacity</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_SettingsCard.Header" xml:space="preserve">
<value>Material</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_SettingsCard.Description" xml:space="preserve">
<value>Select the visual material used for the window background</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_Acrylic.Content" xml:space="preserve">
<value>Acrylic (default)</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_Transparent.Content" xml:space="preserve">
<value>Transparent</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_Mica.Content" xml:space="preserve">
<value>Mica</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_AcrylicThin.Content" xml:space="preserve">
<value>Thin Acrylic</value>
</data>
<data name="Settings_GeneralPage_BackdropStyle_MicaAlt.Content" xml:space="preserve">
<value>Mica Alt</value>
</data>
<data name="Settings_GeneralPage_MicaBackdrop_DescriptionTextBlock.Text" xml:space="preserve">
<value>Mica automatically adapts to your desktop wallpaper. Custom backgrounds and opacity settings are not available for this material.</value>
</data>
<data name="Settings_AppearancePage_OpenCommandPaletteButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette</value>
<comment>Button to open the Command Palette window to preview appearance changes</comment>
</data>
<data name="Settings_AppearancePage_OpenCommandPaletteButton_Text.Text" xml:space="preserve">
<value>Open Command Palette</value>
</data>
<data name="Settings_AppearancePage_ResetAppearanceButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Reset appearance settings</value>
<comment>Button to reset all appearance settings to their default values</comment>
</data>
<data name="Settings_AppearancePage_ResetAppearanceButton_Text.Text" xml:space="preserve">
<value>Reset to defaults</value>
</data>
<data name="Settings_ExtensionsPage_More_Button.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>More options</value>
</data>
</root>

View File

@@ -94,48 +94,39 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
// Arguments:
// - path: the full, expanded path to the icon.
// - targetSize: the target size for decoding/rasterizing the icon.
// Return Value:
// - An IconElement with its IconSource set, if possible.
template<typename TIconSource>
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, int targetSize)
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome)
{
// FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters.
// To skip throwing on Uri construction, we can quickly check if the first character is ASCII.
if (path.empty() || path.front() >= 128)
if (!path.empty() && path.front() < 128)
{
return nullptr;
}
try
{
winrt::Windows::Foundation::Uri iconUri{ path };
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
try
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
source.RasterizePixelWidth(static_cast<double>(targetSize));
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// source.RasterizePixelHeight(static_cast<double>(targetSize));
iconSource.ImageSource(source);
return iconSource;
}
else
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::BitmapImage bitmapImage;
bitmapImage.DecodePixelWidth(targetSize);
// Set only single dimension here; the image might not be square and
// this will preserve the aspect ratio (for the price of keeping height unbound).
// bitmapImage.DecodePixelHeight(targetSize);
bitmapImage.UriSource(iconUri);
iconSource.ImageSource(bitmapImage);
return iconSource;
winrt::Windows::Foundation::Uri iconUri{ path };
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
{
typename ImageIconSource<TIconSource>::type iconSource;
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
iconSource.ImageSource(source);
return iconSource;
}
else
{
typename BitmapIconSource<TIconSource>::type iconSource;
// Make sure to set this to false, so we keep the RGB data of the
// image. Otherwise, the icon will be white for all the
// non-transparent pixels in the image.
iconSource.ShowAsMonochrome(monochrome);
iconSource.UriSource(iconUri);
return iconSource;
}
}
CATCH_LOG();
}
CATCH_LOG();
return nullptr;
}
@@ -167,14 +158,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// Return Value:
// - An IconElement with its IconSource set, if possible.
template<typename TIconSource>
TIconSource _getIconSource(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize)
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
{
TIconSource iconSource{ nullptr };
if (iconPath.size() != 0)
{
const auto expandedIconPath{ _expandIconPath(iconPath) };
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, targetSize);
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, monochrome);
// If we fail to set the icon source using the "icon" as a path,
// let's try it as a symbol/emoji.
@@ -244,9 +235,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
// }
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, const winrt::hstring& fontFamily, const int targetSize)
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
{
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, fontFamily, targetSize);
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
}
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
@@ -361,6 +352,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
}
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
const bool monochrome,
const winrt::hstring& fontFamily,
const int targetSize)
{
@@ -368,7 +360,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
if (!indexOpt.has_value())
{
return _IconSourceMUX(iconPath, fontFamily, targetSize);
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
}
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
@@ -382,14 +374,13 @@ namespace winrt::Microsoft::Terminal::UI::implementation
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) {
return IconMUX(iconPath, 24);
}
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize)
{
std::wstring_view iconPathWithoutIndex;
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
if (!indexOpt.has_value())
{
auto source = IconSourceMUX(iconPath, L"", targetSize);
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
icon.IconSource(source);
return icon;

View File

@@ -10,7 +10,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
//static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath);
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath);
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize=24);
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);

View File

@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
{
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, String fontFamily, Int32 targetSize);
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily, Int32 targetSize);
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
};

View File

@@ -201,7 +201,7 @@
<None Include="Microsoft.Terminal.UI.def" />
</ItemGroup>
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutDir>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutDir>
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

View File

@@ -1,78 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public sealed class PrecomputedFuzzyMatcherEmojiTests
{
private readonly PrecomputedFuzzyMatcher _matcher = new();
[TestMethod]
public void ExactMatch_SimpleEmoji_ReturnsScore()
{
const string needle = "🚀";
const string haystack = "Launch 🚀 sequence";
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for simple emoji");
}
[TestMethod]
public void ExactMatch_SkinTone_ReturnsScore()
{
const string needle = "👍🏽"; // Medium skin tone
const string haystack = "Thumbs up 👍🏽 here";
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for emoji with skin tone");
}
[TestMethod]
public void ZWJSequence_Family_Match()
{
const string needle = "👨‍👩‍👧‍👦"; // Family: Man, Woman, Girl, Boy
const string haystack = "Emoji 👨‍👩‍👧‍👦 Test";
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for ZWJ sequence");
}
[TestMethod]
public void Flags_Match()
{
const string needle = "🇺🇸"; // US Flag (Regional Indicator U + Regional Indicator S)
const string haystack = "USA 🇺🇸";
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for flag emoji");
}
[TestMethod]
public void Emoji_MixedWithText_Search()
{
const string needle = "t🌮o"; // "t" + taco + "o"
const string haystack = "taco 🌮 on tuesday";
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for emoji mixed with text");
}
}

View File

@@ -1,84 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public sealed class PrecomputedFuzzyMatcherOptionsTests
{
[TestMethod]
public void Score_RemoveDiacriticsOption_AffectsMatching()
{
var withDiacriticsRemoved = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true });
var withoutDiacriticsRemoved = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = false });
const string needle = "cafe";
const string haystack = "CAFÉ";
var scoreWithRemoval = withDiacriticsRemoved.Score(
withDiacriticsRemoved.PrecomputeQuery(needle),
withDiacriticsRemoved.PrecomputeTarget(haystack));
var scoreWithoutRemoval = withoutDiacriticsRemoved.Score(
withoutDiacriticsRemoved.PrecomputeQuery(needle),
withoutDiacriticsRemoved.PrecomputeTarget(haystack));
Assert.IsTrue(scoreWithRemoval > 0, "Expected match when diacritics are removed.");
Assert.AreEqual(0, scoreWithoutRemoval, "Expected no match when diacritics are preserved.");
}
[TestMethod]
public void Score_SkipWordSeparatorsOption_AffectsMatching()
{
var skipSeparators = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { SkipWordSeparators = true });
var keepSeparators = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { SkipWordSeparators = false });
const string needle = "a b";
const string haystack = "ab";
var scoreSkip = skipSeparators.Score(
skipSeparators.PrecomputeQuery(needle),
skipSeparators.PrecomputeTarget(haystack));
var scoreKeep = keepSeparators.Score(
keepSeparators.PrecomputeQuery(needle),
keepSeparators.PrecomputeTarget(haystack));
Assert.IsTrue(scoreSkip > 0, "Expected match when word separators are skipped.");
Assert.AreEqual(0, scoreKeep, "Expected no match when word separators are preserved.");
}
[TestMethod]
public void Score_IgnoreSameCaseBonusOption_AffectsLowercaseQuery()
{
var ignoreSameCase = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions
{
IgnoreSameCaseBonusIfQueryIsAllLowercase = true,
SameCaseBonus = 10,
});
var applySameCase = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions
{
IgnoreSameCaseBonusIfQueryIsAllLowercase = false,
SameCaseBonus = 10,
});
const string needle = "test";
const string haystack = "test";
var scoreIgnore = ignoreSameCase.Score(
ignoreSameCase.PrecomputeQuery(needle),
ignoreSameCase.PrecomputeTarget(haystack));
var scoreApply = applySameCase.Score(
applySameCase.PrecomputeQuery(needle),
applySameCase.PrecomputeTarget(haystack));
Assert.IsTrue(scoreApply > scoreIgnore, "Expected same-case bonus to apply when not ignored.");
}
}

View File

@@ -1,227 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public sealed class PrecomputedFuzzyMatcherSecondaryInputTests
{
private readonly PrecomputedFuzzyMatcher _matcher = new();
private readonly StringFolder _folder = new();
private readonly BloomFilter _bloom = new();
[TestMethod]
public void Score_PrimaryQueryMatchesSecondaryTarget_ShouldMatch()
{
// Scenario: Searching for "calc" should match a file "calculator.exe" where primary is filename, secondary is path
var query = CreateQuery("calc");
var target = CreateTarget(primary: "important.txt", secondary: "C:\\Programs\\Calculator\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected primary query to match secondary target");
}
[TestMethod]
public void Score_SecondaryQueryMatchesPrimaryTarget_ShouldMatch()
{
// Scenario: User types "documents\\report" and we want to match against filename
var query = CreateQuery(primary: "documents", secondary: "report");
var target = CreateTarget(primary: "report.docx");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected secondary query to match primary target");
}
[TestMethod]
public void Score_SecondaryQueryMatchesSecondaryTarget_ShouldMatch()
{
// Scenario: Both query and target have secondary info that matches
var query = CreateQuery(primary: "test", secondary: "documents");
var target = CreateTarget(primary: "something.txt", secondary: "C:\\Users\\Documents\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected secondary query to match secondary target");
}
[TestMethod]
public void Score_PrimaryQueryMatchesBothTargets_ShouldReturnBestScore()
{
// The same query matches both primary and secondary of target
var query = CreateQuery("test");
var target = CreateTarget(primary: "test.txt", secondary: "test_folder\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected query to match when it appears in both primary and secondary");
}
[TestMethod]
public void Score_NoSecondaryInQuery_MatchesSecondaryTarget()
{
// Query without secondary can still match target's secondary
var query = CreateQuery("downloads");
var target = CreateTarget(primary: "file.txt", secondary: "C:\\Downloads\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected primary query to match secondary target");
}
[TestMethod]
public void Score_NoSecondaryInTarget_SecondaryQueryShouldNotMatch()
{
// Query with secondary but target without secondary - secondary query shouldn't interfere
var query = CreateQuery(primary: "test", secondary: "extra");
var target = CreateTarget(primary: "test.txt");
var score = _matcher.Score(query, target);
// Primary should still match, secondary query just doesn't contribute
Assert.IsTrue(score > 0, "Expected primary query to match primary target");
}
[TestMethod]
public void Score_SecondaryQueryNoMatch_PrimaryCanStillMatch()
{
// Secondary doesn't match anything, but primary does
var query = CreateQuery(primary: "file", secondary: "nomatch");
var target = CreateTarget(primary: "myfile.txt", secondary: "C:\\Documents\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected primary query to match even when secondary doesn't");
}
[TestMethod]
public void Score_OnlySecondaryMatches_ShouldReturnScore()
{
// Only the secondary parts match, primary doesn't
var query = CreateQuery(primary: "xyz", secondary: "documents");
var target = CreateTarget(primary: "abc.txt", secondary: "C:\\Users\\Documents\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match when only secondary parts match");
}
[TestMethod]
public void Score_BothQueriesMatchDifferentTargets_ShouldReturnBestScore()
{
// Primary query matches secondary target, secondary query matches primary target
var query = CreateQuery(primary: "docs", secondary: "report");
var target = CreateTarget(primary: "report.pdf", secondary: "C:\\Documents\\");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match when queries cross-match with targets");
}
[TestMethod]
public void Score_CompletelyDifferent_ShouldNotMatch()
{
var query = CreateQuery(primary: "xyz", secondary: "abc");
var target = CreateTarget(primary: "hello", secondary: "world");
var score = _matcher.Score(query, target);
Assert.AreEqual(0, score, "Expected no match when nothing matches");
}
[TestMethod]
public void Score_EmptySecondaryInputs_ShouldMatchOnPrimary()
{
var query = CreateQuery(primary: "test", secondary: string.Empty);
var target = CreateTarget(primary: "test.txt", secondary: string.Empty);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match on primary when secondaries are empty");
}
[TestMethod]
public void Score_WordSeparatorMatching_AcrossSecondary()
{
// Test that "Power Point" matches "PowerPoint" using secondary
var query = CreateQuery(primary: "power", secondary: "point");
var target = CreateTarget(primary: "PowerPoint.exe");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected 'power' + 'point' to match 'PowerPoint'");
}
private FuzzyQuery CreateQuery(string primary, string? secondary = null)
{
var primaryFolded = _folder.Fold(primary, removeDiacritics: true);
var primaryBloom = _bloom.Compute(primaryFolded);
var primaryEffectiveLength = primaryFolded.Length;
var primaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(primary);
string? secondaryFolded = null;
ulong secondaryBloom = 0;
var secondaryEffectiveLength = 0;
var secondaryIsAllLowercase = true;
if (!string.IsNullOrEmpty(secondary))
{
secondaryFolded = _folder.Fold(secondary, removeDiacritics: true);
secondaryBloom = _bloom.Compute(secondaryFolded);
secondaryEffectiveLength = secondaryFolded.Length;
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondary);
}
return new FuzzyQuery(
original: primary,
folded: primaryFolded,
bloom: primaryBloom,
effectiveLength: primaryEffectiveLength,
isAllLowercaseAsciiOrNonLetter: primaryIsAllLowercase,
secondaryOriginal: secondary,
secondaryFolded: secondaryFolded,
secondaryBloom: secondaryBloom,
secondaryEffectiveLength: secondaryEffectiveLength,
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
}
private FuzzyTarget CreateTarget(string primary, string? secondary = null)
{
var primaryFolded = _folder.Fold(primary, removeDiacritics: true);
var primaryBloom = _bloom.Compute(primaryFolded);
string? secondaryFolded = null;
ulong secondaryBloom = 0;
if (!string.IsNullOrEmpty(secondary))
{
secondaryFolded = _folder.Fold(secondary, removeDiacritics: true);
secondaryBloom = _bloom.Compute(secondaryFolded);
}
return new FuzzyTarget(
original: primary,
folded: primaryFolded,
bloom: primaryBloom,
secondaryOriginal: secondary,
secondaryFolded: secondaryFolded,
secondaryBloom: secondaryBloom);
}
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
{
foreach (var c in s)
{
if ((uint)(c - 'A') <= ('Z' - 'A'))
{
return false;
}
}
return true;
}
}

View File

@@ -1,209 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public class PrecomputedFuzzyMatcherTests
{
private readonly PrecomputedFuzzyMatcher _matcher = new();
public static IEnumerable<object[]> MatchData =>
[
["a", "a"],
["abc", "abc"],
["a", "ab"],
["b", "ab"],
["abc", "axbycz"],
["pt", "PowerToys"],
["calc", "Calculator"],
["vs", "Visual Studio"],
["code", "Visual Studio Code"],
// Diacritics
["abc", "ÁBC"],
// Separators
["p/t", "power\\toys"],
];
public static IEnumerable<object[]> NonMatchData =>
[
["z", "abc"],
["verylongstring", "short"],
];
[TestMethod]
[DynamicData(nameof(MatchData))]
public void Score_Matches_ShouldHavePositiveScore(string needle, string haystack)
{
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, $"Expected positive score for needle='{needle}', haystack='{haystack}'");
}
[TestMethod]
[DynamicData(nameof(NonMatchData))]
public void Score_NonMatches_ShouldHaveZeroScore(string needle, string haystack)
{
var query = _matcher.PrecomputeQuery(needle);
var target = _matcher.PrecomputeTarget(haystack);
var score = _matcher.Score(query, target);
Assert.AreEqual(0, score, $"Expected 0 score for needle='{needle}', haystack='{haystack}'");
}
[TestMethod]
public void Score_EmptyQuery_ReturnsZero()
{
var query = _matcher.PrecomputeQuery(string.Empty);
var target = _matcher.PrecomputeTarget("something");
Assert.AreEqual(0, _matcher.Score(query, target));
}
[TestMethod]
public void Score_EmptyTarget_ReturnsZero()
{
var query = _matcher.PrecomputeQuery("something");
var target = _matcher.PrecomputeTarget(string.Empty);
Assert.AreEqual(0, _matcher.Score(query, target));
}
[TestMethod]
public void SchemaId_DefaultMatcher_IsConsistent()
{
var matcher1 = new PrecomputedFuzzyMatcher();
var matcher2 = new PrecomputedFuzzyMatcher();
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Default matchers should have the same SchemaId");
}
[TestMethod]
public void SchemaId_SameOptions_ProducesSameId()
{
var options = new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true };
var matcher1 = new PrecomputedFuzzyMatcher(options);
var matcher2 = new PrecomputedFuzzyMatcher(options);
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Matchers with same options should have the same SchemaId");
}
[TestMethod]
public void SchemaId_DifferentRemoveDiacriticsOption_ProducesDifferentId()
{
var matcherWithDiacriticsRemoval = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = true });
var matcherWithoutDiacriticsRemoval = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { RemoveDiacritics = false });
Assert.AreNotEqual(
matcherWithDiacriticsRemoval.SchemaId,
matcherWithoutDiacriticsRemoval.SchemaId,
"Different RemoveDiacritics option should produce different SchemaId");
}
[TestMethod]
public void SchemaId_ScoringOptionsDoNotAffectId()
{
// SchemaId should only be affected by options that affect folding/bloom, not scoring
var matcher1 = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { CharMatchBonus = 1, CamelCaseBonus = 2 });
var matcher2 = new PrecomputedFuzzyMatcher(
new PrecomputedFuzzyMatcherOptions { CharMatchBonus = 100, CamelCaseBonus = 200 });
Assert.AreEqual(matcher1.SchemaId, matcher2.SchemaId, "Scoring options should not affect SchemaId");
}
[TestMethod]
public void Score_WordSeparatorMatching_PowerPoint()
{
// Test that "Power Point" can match "PowerPoint" when word separators are skipped
var query = _matcher.PrecomputeQuery("Power Point");
var target = _matcher.PrecomputeTarget("PowerPoint");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected 'Power Point' to match 'PowerPoint'");
}
[TestMethod]
public void Score_WordSeparatorMatching_UnderscoreDash()
{
// Test that different word separators match each other
var query = _matcher.PrecomputeQuery("hello_world");
var target = _matcher.PrecomputeTarget("hello-world");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected 'hello_world' to match 'hello-world'");
}
[TestMethod]
public void Score_WordSeparatorMatching_MixedSeparators()
{
// Test multiple different separators
var query = _matcher.PrecomputeQuery("my.file_name");
var target = _matcher.PrecomputeTarget("my-file.name");
var score = _matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected mixed separators to match");
}
[TestMethod]
public void Score_PrecomputedQueryReuse_ShouldWorkConsistently()
{
// Test that precomputed query can be reused across multiple targets
var query = _matcher.PrecomputeQuery("test");
var target1 = _matcher.PrecomputeTarget("test123");
var target2 = _matcher.PrecomputeTarget("mytest");
var target3 = _matcher.PrecomputeTarget("unrelated");
var score1 = _matcher.Score(query, target1);
var score2 = _matcher.Score(query, target2);
var score3 = _matcher.Score(query, target3);
Assert.IsTrue(score1 > 0, "Expected query to match first target");
Assert.IsTrue(score2 > 0, "Expected query to match second target");
Assert.AreEqual(0, score3, "Expected query not to match third target");
}
[TestMethod]
public void Score_PrecomputedTargetReuse_ShouldWorkConsistently()
{
// Test that precomputed target can be reused across multiple queries
var target = _matcher.PrecomputeTarget("calculator");
var query1 = _matcher.PrecomputeQuery("calc");
var query2 = _matcher.PrecomputeQuery("lator");
var query3 = _matcher.PrecomputeQuery("xyz");
var score1 = _matcher.Score(query1, target);
var score2 = _matcher.Score(query2, target);
var score3 = _matcher.Score(query3, target);
Assert.IsTrue(score1 > 0, "Expected first query to match target");
Assert.IsTrue(score2 > 0, "Expected second query to match target");
Assert.AreEqual(0, score3, "Expected third query not to match target");
}
[TestMethod]
public void Score_CaseInsensitiveMatching_Works()
{
// Test various case combinations
var query1 = _matcher.PrecomputeQuery("test");
var query2 = _matcher.PrecomputeQuery("TEST");
var query3 = _matcher.PrecomputeQuery("TeSt");
var target = _matcher.PrecomputeTarget("TestFile");
var score1 = _matcher.Score(query1, target);
var score2 = _matcher.Score(query2, target);
var score3 = _matcher.Score(query3, target);
Assert.IsTrue(score1 > 0, "Expected lowercase query to match");
Assert.IsTrue(score2 > 0, "Expected uppercase query to match");
Assert.IsTrue(score3 > 0, "Expected mixed case query to match");
}
}

View File

@@ -1,124 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public sealed class PrecomputedFuzzyMatcherUnicodeTests
{
private readonly PrecomputedFuzzyMatcher _defaultMatcher = new();
[TestMethod]
public void UnpairedHighSurrogateInNeedle_ShouldNotThrow()
{
const string needle = "\uD83D"; // high surrogate (unpaired)
const string haystack = "abc";
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void UnpairedLowSurrogateInNeedle_ShouldNotThrow()
{
const string needle = "\uDC00"; // low surrogate (unpaired)
const string haystack = "abc";
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void UnpairedHighSurrogateInHaystack_ShouldNotThrow()
{
const string needle = "a";
const string haystack = "a\uD83D" + "bc"; // inject unpaired high surrogate
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void MixedSurrogatesAndMarks_ShouldNotThrow()
{
// "Garbage smoothie": unpaired surrogate + combining mark + emoji surrogate pair
const string needle = "a\uD83D\u0301"; // 'a' + unpaired high surrogate + combining acute
const string haystack = "a\u0301 \U0001F600"; // 'a' + combining acute + space + 😀 (valid pair)
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void ValidEmojiSurrogatePair_ShouldNotThrow_AndCanMatch()
{
// 😀 U+1F600 encoded as surrogate pair in UTF-16
const string needle = "\U0001F600";
const string haystack = "x \U0001F600 y";
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
var score = _defaultMatcher.Score(q, t);
Assert.IsTrue(score > 0, "Expected emoji to produce a match score > 0.");
}
[TestMethod]
public void RandomUtf16Garbage_ShouldNotThrow()
{
// Deterministic pseudo-random "UTF-16 garbage", including surrogates.
var s1 = MakeDeterministicGarbage(seed: 1234, length: 512);
var s2 = MakeDeterministicGarbage(seed: 5678, length: 1024);
var q = _defaultMatcher.PrecomputeQuery(s1);
var t = _defaultMatcher.PrecomputeTarget(s2);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void HighSurrogateAtEndOfHaystack_ShouldNotThrow()
{
const string needle = "a";
const string haystack = "abc\uD83D"; // Ends with high surrogate
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
[TestMethod]
public void VeryLongStrings_ShouldNotThrow()
{
var needle = new string('a', 100);
var haystack = new string('b', 10000) + needle + new string('c', 10000);
var q = _defaultMatcher.PrecomputeQuery(needle);
var t = _defaultMatcher.PrecomputeTarget(haystack);
_ = _defaultMatcher.Score(q, t);
}
private static string MakeDeterministicGarbage(int seed, int length)
{
// LCG for deterministic generation without Randoms platform/version surprises.
var x = (uint)seed;
var chars = length <= 2048 ? stackalloc char[length] : new char[length];
for (var i = 0; i < chars.Length; i++)
{
// LCG: x = (a*x + c) mod 2^32
x = unchecked((1664525u * x) + 1013904223u);
// Take top 16 bits as UTF-16 code unit (includes surrogates).
chars[i] = (char)(x >> 16);
}
return new string(chars);
}
}

View File

@@ -1,117 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public class PrecomputedFuzzyMatcherWithPinyinTests
{
private PrecomputedFuzzyMatcherWithPinyin CreateMatcher(PinyinMode mode = PinyinMode.On, bool removeApostrophes = true)
{
return new PrecomputedFuzzyMatcherWithPinyin(
new PrecomputedFuzzyMatcherOptions(),
new PinyinFuzzyMatcherOptions { Mode = mode, RemoveApostrophesForQuery = removeApostrophes },
new StringFolder(),
new BloomFilter());
}
[TestMethod]
[DataRow("bj", "北京")]
[DataRow("sh", "上海")]
[DataRow("nihao", "你好")]
[DataRow("beijing", "北京")]
[DataRow("ce", "测试")]
public void Score_PinyinMatches_ShouldHavePositiveScore(string needle, string haystack)
{
var matcher = CreateMatcher(PinyinMode.On);
var query = matcher.PrecomputeQuery(needle);
var target = matcher.PrecomputeTarget(haystack);
var score = matcher.Score(query, target);
Assert.IsTrue(score > 0, $"Expected positive score for needle='{needle}', haystack='{haystack}'");
}
[TestMethod]
public void Score_PinyinOff_ShouldNotMatchPinyin()
{
var matcher = CreateMatcher(PinyinMode.Off);
var needle = "bj";
var haystack = "北京";
var query = matcher.PrecomputeQuery(needle);
var target = matcher.PrecomputeTarget(haystack);
var score = matcher.Score(query, target);
Assert.AreEqual(0, score, "Pinyin match should be disabled.");
}
[TestMethod]
public void Score_StandardMatch_WorksWithPinyinMatcher()
{
var matcher = CreateMatcher(PinyinMode.On);
var needle = "abc";
var haystack = "abc";
var query = matcher.PrecomputeQuery(needle);
var target = matcher.PrecomputeTarget(haystack);
var score = matcher.Score(query, target);
Assert.IsTrue(score > 0, "Standard match should still work.");
}
[TestMethod]
public void Score_ApostropheRemoval_Works()
{
var matcher = CreateMatcher(PinyinMode.On, removeApostrophes: true);
var needle = "xi'an";
// "xi'an" -> "xian" -> matches "西安" (Xi An)
var haystack = "西安";
var query = matcher.PrecomputeQuery(needle);
var target = matcher.PrecomputeTarget(haystack);
var score = matcher.Score(query, target);
Assert.IsTrue(score > 0, "Expected match for 'xi'an' -> '西安' with apostrophe removal.");
}
[TestMethod]
public void AutoMode_EnablesForChineseCulture()
{
var originalCulture = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = new CultureInfo("zh-CN");
var matcher = CreateMatcher(PinyinMode.AutoSimplifiedChineseUi);
var score = matcher.Score(matcher.PrecomputeQuery("bj"), matcher.PrecomputeTarget("北京"));
Assert.IsTrue(score > 0, "Should match when UI culture is zh-CN");
}
finally
{
CultureInfo.CurrentUICulture = originalCulture;
}
}
[TestMethod]
public void AutoMode_DisablesForNonChineseCulture()
{
var originalCulture = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = new CultureInfo("en-US");
var matcher = CreateMatcher(PinyinMode.AutoSimplifiedChineseUi);
var score = matcher.Score(matcher.PrecomputeQuery("bj"), matcher.PrecomputeTarget("北京"));
Assert.AreEqual(0, score, "Should NOT match when UI culture is en-US");
}
finally
{
CultureInfo.CurrentUICulture = originalCulture;
}
}
}

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Common.UnitTests.Text;
[TestClass]
public class StringFolderTests
{
private readonly StringFolder _folder = new();
[TestMethod]
[DataRow(null, "")]
[DataRow("", "")]
[DataRow("abc", "ABC")]
[DataRow("ABC", "ABC")]
[DataRow("a\\b", "A/B")]
[DataRow("a/b", "A/B")]
[DataRow("ÁBC", "ABC")] // Diacritic removal
[DataRow("ñ", "N")]
[DataRow("hello world", "HELLO WORLD")]
public void Fold_RemoveDiacritics_Works(string input, string expected)
{
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: true));
}
[TestMethod]
[DataRow("abc", "ABC")]
[DataRow("ÁBC", "ÁBC")] // No diacritic removal
[DataRow("a\\b", "A/B")]
public void Fold_KeepDiacritics_Works(string input, string expected)
{
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: false));
}
[TestMethod]
public void Fold_IsAlreadyFolded_ReturnsSameInstance()
{
var input = "ALREADY/FOLDED";
var result = _folder.Fold(input, removeDiacritics: true);
Assert.AreSame(input, result);
}
[TestMethod]
public void Fold_WithNonAsciiButNoDiacritics_ReturnsFolded()
{
// E.g. Cyrillic or other scripts that might not decompose in a simple way or just upper case
// "привет" -> "ПРИВЕТ"
var input = "привет";
var expected = "ПРИВЕТ";
Assert.AreEqual(expected, _folder.Fold(input, removeDiacritics: true));
}
}

View File

@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -44,34 +43,38 @@ public partial class MainListPageResultFactoryTests
public override string ToString() => Title;
}
private static RoScored<IListItem> S(string title, int score)
private static Scored<IListItem> S(string title, int score)
{
return new RoScored<IListItem>(score: score, item: new MockListItem { Title = title });
return new Scored<IListItem>
{
Score = score,
Item = new MockListItem { Title = title },
};
}
[TestMethod]
public void Merge_PrioritizesListsCorrectly()
{
var filtered = new List<RoScored<IListItem>>
var filtered = new List<Scored<IListItem>>
{
S("F1", 100),
S("F2", 50),
};
var scoredFallback = new List<RoScored<IListItem>>
var scoredFallback = new List<Scored<IListItem>>
{
S("SF1", 100),
S("SF2", 60),
};
var apps = new List<RoScored<IListItem>>
var apps = new List<Scored<IListItem>>
{
S("A1", 100),
S("A2", 55),
};
// Fallbacks are not scored.
var fallbacks = new List<RoScored<IListItem>>
var fallbacks = new List<Scored<IListItem>>
{
S("FB1", 0),
S("FB2", 0),
@@ -101,7 +104,7 @@ public partial class MainListPageResultFactoryTests
[TestMethod]
public void Merge_AppliesAppLimit()
{
var apps = new List<RoScored<IListItem>>
var apps = new List<Scored<IListItem>>
{
S("A1", 100),
S("A2", 90),
@@ -123,7 +126,7 @@ public partial class MainListPageResultFactoryTests
[TestMethod]
public void Merge_FiltersEmptyFallbacks()
{
var fallbacks = new List<RoScored<IListItem>>
var fallbacks = new List<Scored<IListItem>>
{
S("FB1", 0),
S("FB3", 0),

View File

@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CommandPalette.Extensions;
@@ -264,12 +263,10 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
};
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
var fuzzyMatcher = CreateMatcher();
var q = fuzzyMatcher.PrecomputeQuery("C");
var scoreA = MainListPage.ScoreTopLevelItem(q, items[0], history, fuzzyMatcher);
var scoreB = MainListPage.ScoreTopLevelItem(q, items[1], history, fuzzyMatcher);
var scoreC = MainListPage.ScoreTopLevelItem(q, items[2], history, fuzzyMatcher);
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
// Assert
// All of these equally match the query, and they're all in the same bucket,
@@ -299,11 +296,6 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
return history;
}
private static IPrecomputedFuzzyMatcher CreateMatcher()
{
return new PrecomputedFuzzyMatcher(new PrecomputedFuzzyMatcherOptions());
}
private sealed record ScoredItem(ListItemMock Item, int Score)
{
public string Title => Item.Title;
@@ -345,11 +337,9 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var fuzzyMatcher = CreateMatcher();
var q = fuzzyMatcher.PrecomputeQuery("C");
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, emptyHistory, fuzzyMatcher)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
for (var i = 0; i < unweightedScores.Count; i++)
{
@@ -390,10 +380,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var fuzzyMatcher = CreateMatcher();
var q = fuzzyMatcher.PrecomputeQuery("te");
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
@@ -411,8 +398,6 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var fuzzyMatcher = CreateMatcher();
var q = fuzzyMatcher.PrecomputeQuery("te");
// Add extra uses of VS Code to try and push it above Terminal
for (var i = 0; i < 10; i++)
@@ -420,7 +405,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
history.AddHistoryItem(items[1].Id);
}
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
@@ -438,8 +423,6 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
var items = CreateMockHistoryItems();
var emptyHistory = CreateMockHistoryService(new());
var history = CreateMockHistoryService(items);
var fuzzyMatcher = CreateMatcher();
var q = fuzzyMatcher.PrecomputeQuery("C");
// We're gonna run this test and keep adding more uses of VS Code till
// it breaks past Command Prompt
@@ -448,7 +431,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
{
history.AddHistoryItem(vsCodeId);
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
var weightedMatches = GetMatches(items, weightedScores).ToList();
Assert.AreEqual(4, weightedMatches.Count);

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Text;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CommandPalette.Extensions;
@@ -15,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
public sealed partial class AppListItem : ListItem, IPrecomputedListItem
public sealed partial class AppListItem : ListItem
{
private readonly AppCommand _appCommand;
private readonly AppItem _app;
@@ -26,35 +25,6 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
private InterlockedBoolean _isLoadingIcon;
private InterlockedBoolean _isLoadingDetails;
private FuzzyTargetCache _titleCache;
private FuzzyTargetCache _subtitleCache;
public override string Title
{
get => base.Title;
set
{
if (!string.Equals(base.Title, value, StringComparison.Ordinal))
{
base.Title = value;
_titleCache.Invalidate();
}
}
}
public override string Subtitle
{
get => base.Subtitle;
set
{
if (!string.Equals(value, base.Subtitle, StringComparison.Ordinal))
{
base.Subtitle = value;
_subtitleCache.Invalidate();
}
}
}
public override IDetails? Details
{
get
@@ -289,10 +259,4 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
return null;
}).ConfigureAwait(false);
}
public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _titleCache.GetOrUpdate(matcher, Title);
public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
=> _subtitleCache.GetOrUpdate(matcher, Subtitle);
}

View File

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

View File

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

View File

@@ -15,6 +15,8 @@
<Nullable>enable</Nullable>
<ProjectGuid>{AF2349B8-E5B6-4004-9502-687C1C7730B1}</ProjectGuid>
<AssemblyName>PowerToys.PreviewHandlerCommon</AssemblyName>
<!-- AutoUnify resolves WindowsBase version conflict (WebView2.Wpf refs .NET 5 version, we target .NET 9) -->
<AutoUnifyAssemblyReferences>true</AutoUnifyAssemblyReferences>
</PropertyGroup>
<ItemGroup>

View File

@@ -173,7 +173,7 @@ void ProcessNewVersionInfo(const github_version_info& version_info,
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
if (download_new_version(new_version_info).get())
if (download_new_version(new_version_info))
{
state.state = UpdateState::readyToInstall;
state.downloadedInstallerFilename = new_version_info.installer_filename;
@@ -232,7 +232,7 @@ void PeriodicUpdateWorker()
bool version_info_obtained = false;
try
{
const auto new_version_info = get_github_version_info_async().get();
const auto new_version_info = get_github_version_info_async();
if (new_version_info.has_value())
{
version_info_obtained = true;
@@ -272,7 +272,7 @@ void CheckForUpdatesCallback()
auto state = UpdateState::read();
try
{
auto new_version_info = get_github_version_info_async().get();
auto new_version_info = get_github_version_info_async();
if (!new_version_info)
{
// We couldn't get a new version from github for some reason, log error