mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-20 18:20:27 +01:00
Compare commits
7 Commits
async-cpp-
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52f85faec | ||
|
|
521d34f1eb | ||
|
|
a02a5a9736 | ||
|
|
1e25d17920 | ||
|
|
4959273875 | ||
|
|
095961402b | ||
|
|
7477b561a1 |
1
.github/actions/spell-check/excludes.txt
vendored
1
.github/actions/spell-check/excludes.txt
vendored
@@ -111,6 +111,7 @@
|
|||||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.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/.*\.TestData\.cs$
|
||||||
|
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/Text/.*\.cs$
|
||||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||||
|
|||||||
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -1527,6 +1527,7 @@ randi
|
|||||||
RAquadrant
|
RAquadrant
|
||||||
rasterization
|
rasterization
|
||||||
Rasterize
|
Rasterize
|
||||||
|
rasterizing
|
||||||
RAWINPUTDEVICE
|
RAWINPUTDEVICE
|
||||||
RAWINPUTHEADER
|
RAWINPUTHEADER
|
||||||
RAWMODE
|
RAWMODE
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
<TreatWarningAsError>true</TreatWarningAsError>
|
<TreatWarningAsError>true</TreatWarningAsError>
|
||||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||||
<BuildStlModules>false</BuildStlModules>
|
<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. -->
|
<!-- 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>
|
<PreprocessorDefinitions>_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
<!-- CLR + CFG are not compatible >:{ -->
|
<!-- CLR + CFG are not compatible >:{ -->
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <spdlog/sinks/base_sink.h>
|
#include <spdlog/sinks/base_sink.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "../../src/common/logger/logger.h"
|
#include "../../src/common/logger/logger.h"
|
||||||
#include "../../src/common/utils/gpo.h"
|
#include "../../src/common/utils/gpo.h"
|
||||||
@@ -57,6 +59,135 @@ constexpr inline const wchar_t *DataDiagnosticsRegValueName = L"AllowDataDiagnos
|
|||||||
|
|
||||||
static Shared::Trace::ETWTrace trace{L"PowerToys_Installer"};
|
static Shared::Trace::ETWTrace trace{L"PowerToys_Installer"};
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct VersionQuad
|
||||||
|
{
|
||||||
|
uint16_t major = 0;
|
||||||
|
uint16_t minor = 0;
|
||||||
|
uint16_t patch = 0;
|
||||||
|
uint16_t revision = 0;
|
||||||
|
|
||||||
|
bool operator>(const VersionQuad& other) const
|
||||||
|
{
|
||||||
|
return std::tie(major, minor, patch, revision) > std::tie(other.major, other.minor, other.patch, other.revision);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::wstring VersionToWString(const VersionQuad& v)
|
||||||
|
{
|
||||||
|
return std::to_wstring(v.major) + L"." + std::to_wstring(v.minor) + L"." + std::to_wstring(v.patch) + L"." + std::to_wstring(v.revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryGetFileVersion(const std::wstring& filePath, VersionQuad& version)
|
||||||
|
{
|
||||||
|
DWORD dummyHandle = 0;
|
||||||
|
DWORD verSize = GetFileVersionInfoSizeW(filePath.c_str(), &dummyHandle);
|
||||||
|
if (verSize == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BYTE> verData(verSize);
|
||||||
|
if (!GetFileVersionInfoW(filePath.c_str(), 0, verSize, verData.data()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VS_FIXEDFILEINFO* verInfo = nullptr;
|
||||||
|
UINT verInfoSize = 0;
|
||||||
|
if (!VerQueryValueW(verData.data(), L"\\", reinterpret_cast<LPVOID*>(&verInfo), &verInfoSize) || verInfo == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
version.major = HIWORD(verInfo->dwFileVersionMS);
|
||||||
|
version.minor = LOWORD(verInfo->dwFileVersionMS);
|
||||||
|
version.patch = HIWORD(verInfo->dwFileVersionLS);
|
||||||
|
version.revision = LOWORD(verInfo->dwFileVersionLS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPowerToysPerUserProduct(const wchar_t* productCode, const wchar_t* userSid, MSIINSTALLCONTEXT context)
|
||||||
|
{
|
||||||
|
if ((context != MSIINSTALLCONTEXT_USERMANAGED) && (context != MSIINSTALLCONTEXT_USERUNMANAGED))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t componentPath[MAX_PATH]{};
|
||||||
|
DWORD pathLength = MAX_PATH;
|
||||||
|
INSTALLSTATE state = MsiGetComponentPathExW(productCode, POWERTOYS_EXE_COMPONENT, userSid, context, componentPath, &pathLength);
|
||||||
|
return state == INSTALLSTATE_LOCAL || state == INSTALLSTATE_SOURCE || state == INSTALLSTATE_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAnyPowerToysPerUserInstallPresent()
|
||||||
|
{
|
||||||
|
static constexpr wchar_t sidAllUsers[] = L"S-1-1-0";
|
||||||
|
const DWORD contexts = MSIINSTALLCONTEXT_USERMANAGED | MSIINSTALLCONTEXT_USERUNMANAGED;
|
||||||
|
|
||||||
|
for (DWORD index = 0;; ++index)
|
||||||
|
{
|
||||||
|
WCHAR productCode[39]{};
|
||||||
|
WCHAR sidBuffer[256]{};
|
||||||
|
DWORD sidLength = static_cast<DWORD>(std::size(sidBuffer));
|
||||||
|
MSIINSTALLCONTEXT installedContext = MSIINSTALLCONTEXT_NONE;
|
||||||
|
|
||||||
|
UINT enumResult = MsiEnumProductsExW(
|
||||||
|
nullptr,
|
||||||
|
sidAllUsers,
|
||||||
|
contexts,
|
||||||
|
index,
|
||||||
|
productCode,
|
||||||
|
&installedContext,
|
||||||
|
sidBuffer,
|
||||||
|
&sidLength);
|
||||||
|
|
||||||
|
if (enumResult == ERROR_NO_MORE_ITEMS)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enumResult != ERROR_SUCCESS && enumResult != ERROR_MORE_DATA)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring dynamicSid;
|
||||||
|
const wchar_t* sidPtr = sidBuffer[0] ? sidBuffer : nullptr;
|
||||||
|
if (enumResult == ERROR_MORE_DATA)
|
||||||
|
{
|
||||||
|
dynamicSid.resize(sidLength + 1);
|
||||||
|
DWORD retrySidLength = static_cast<DWORD>(dynamicSid.size());
|
||||||
|
enumResult = MsiEnumProductsExW(
|
||||||
|
nullptr,
|
||||||
|
sidAllUsers,
|
||||||
|
contexts,
|
||||||
|
index,
|
||||||
|
productCode,
|
||||||
|
&installedContext,
|
||||||
|
dynamicSid.data(),
|
||||||
|
&retrySidLength);
|
||||||
|
|
||||||
|
if (enumResult != ERROR_SUCCESS)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicSid.resize(retrySidLength);
|
||||||
|
sidPtr = dynamicSid.empty() || dynamicSid[0] == L'\0' ? nullptr : dynamicSid.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPowerToysPerUserProduct(productCode, sidPtr, installedContext))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline bool isDataDiagnosticEnabled()
|
inline bool isDataDiagnosticEnabled()
|
||||||
{
|
{
|
||||||
HKEY key{};
|
HKEY key{};
|
||||||
@@ -337,6 +468,69 @@ LExit:
|
|||||||
return WcaFinalize(er);
|
return WcaFinalize(er);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UINT __stdcall CheckInstallGuardsCA(MSIHANDLE hInstall)
|
||||||
|
{
|
||||||
|
HRESULT hr = S_OK;
|
||||||
|
UINT er = ERROR_SUCCESS;
|
||||||
|
LPWSTR currentScope = nullptr;
|
||||||
|
LPWSTR installFolder = nullptr;
|
||||||
|
|
||||||
|
hr = WcaInitialize(hInstall, "CheckInstallGuardsCA");
|
||||||
|
ExitOnFailure(hr, "Failed to initialize");
|
||||||
|
|
||||||
|
hr = WcaGetProperty(L"InstallScope", ¤tScope);
|
||||||
|
ExitOnFailure(hr, "Failed to get InstallScope property");
|
||||||
|
|
||||||
|
if (currentScope != nullptr && std::wstring{ currentScope } == L"perMachine" && IsAnyPowerToysPerUserInstallPresent())
|
||||||
|
{
|
||||||
|
PMSIHANDLE hRecord = MsiCreateRecord(0);
|
||||||
|
MsiRecordSetStringW(hRecord, 0, L"PowerToys is already installed per-user for at least one account. Please uninstall all per-user PowerToys installations before installing machine-wide.");
|
||||||
|
MsiProcessMessage(hInstall, static_cast<INSTALLMESSAGE>(INSTALLMESSAGE_ERROR + MB_OK), hRecord);
|
||||||
|
hr = E_ABORT;
|
||||||
|
ExitOnFailure(hr, "Per-user installation detected while attempting machine-wide install");
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = WcaGetProperty(L"INSTALLFOLDER", &installFolder);
|
||||||
|
ExitOnFailure(hr, "Failed to get INSTALLFOLDER property");
|
||||||
|
|
||||||
|
if (installFolder && *installFolder != L'\0')
|
||||||
|
{
|
||||||
|
const auto installedExePath = std::filesystem::path(installFolder) / L"PowerToys.exe";
|
||||||
|
if (std::filesystem::exists(installedExePath))
|
||||||
|
{
|
||||||
|
VersionQuad existingVersion;
|
||||||
|
if (TryGetFileVersion(installedExePath.wstring(), existingVersion))
|
||||||
|
{
|
||||||
|
const VersionQuad targetVersion{
|
||||||
|
static_cast<uint16_t>(VERSION_MAJOR),
|
||||||
|
static_cast<uint16_t>(VERSION_MINOR),
|
||||||
|
static_cast<uint16_t>(VERSION_REVISION),
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingVersion > targetVersion)
|
||||||
|
{
|
||||||
|
const auto existingVersionText = VersionToWString(existingVersion);
|
||||||
|
const auto targetVersionText = VersionToWString(targetVersion);
|
||||||
|
const auto message = L"A newer PowerToys version (" + existingVersionText + L") already exists in the installation folder. The requested installer version (" + targetVersionText + L") is older. Uninstall the newer version first.";
|
||||||
|
|
||||||
|
PMSIHANDLE hRecord = MsiCreateRecord(0);
|
||||||
|
MsiRecordSetStringW(hRecord, 0, message.c_str());
|
||||||
|
MsiProcessMessage(hInstall, static_cast<INSTALLMESSAGE>(INSTALLMESSAGE_ERROR + MB_OK), hRecord);
|
||||||
|
hr = E_ABORT;
|
||||||
|
ExitOnFailure(hr, "A higher PowerToys.exe version already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LExit:
|
||||||
|
ReleaseStr(currentScope);
|
||||||
|
ReleaseStr(installFolder);
|
||||||
|
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
|
||||||
|
return WcaFinalize(er);
|
||||||
|
}
|
||||||
|
|
||||||
// We've deprecated Video Conference Mute. This Custom Action cleans up any stray registry entry for the driver dll.
|
// We've deprecated Video Conference Mute. This Custom Action cleans up any stray registry entry for the driver dll.
|
||||||
UINT __stdcall CleanVideoConferenceRegistryCA(MSIHANDLE hInstall)
|
UINT __stdcall CleanVideoConferenceRegistryCA(MSIHANDLE hInstall)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ LIBRARY "PowerToysSetupCustomActionsVNext"
|
|||||||
EXPORTS
|
EXPORTS
|
||||||
LaunchPowerToysCA
|
LaunchPowerToysCA
|
||||||
CheckGPOCA
|
CheckGPOCA
|
||||||
|
CheckInstallGuardsCA
|
||||||
CleanVideoConferenceRegistryCA
|
CleanVideoConferenceRegistryCA
|
||||||
ApplyModulesRegistryChangeSetsCA
|
ApplyModulesRegistryChangeSetsCA
|
||||||
DetectPrevInstallPathCA
|
DetectPrevInstallPathCA
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<ItemDefinitionGroup>
|
<ItemDefinitionGroup>
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
<AdditionalIncludeDirectories>inc;..\..\src\;..\..\src\common\Telemetry;telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>inc;..\..\src\;..\..\src\common\Telemetry;telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
<AdditionalOptions>/Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
|
<AdditionalOptions>/await /Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
|
||||||
<WarningLevel>Level4</WarningLevel>
|
<WarningLevel>Level4</WarningLevel>
|
||||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
|
|
||||||
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
|
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
|
||||||
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
|
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
|
||||||
|
<Custom Action="CheckInstallGuards" After="CheckGPO" Condition="NOT Installed" />
|
||||||
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||||
@@ -258,6 +259,7 @@
|
|||||||
<CustomAction Id="UnRegisterCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="UnRegisterCmdPalPackageCA" BinaryRef="PTCustomActions" />
|
<CustomAction Id="UnRegisterCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="UnRegisterCmdPalPackageCA" BinaryRef="PTCustomActions" />
|
||||||
|
|
||||||
<CustomAction Id="CheckGPO" Return="check" Impersonate="yes" DllEntry="CheckGPOCA" BinaryRef="PTCustomActions" />
|
<CustomAction Id="CheckGPO" Return="check" Impersonate="yes" DllEntry="CheckGPOCA" BinaryRef="PTCustomActions" />
|
||||||
|
<CustomAction Id="CheckInstallGuards" Return="check" Impersonate="yes" DllEntry="CheckInstallGuardsCA" BinaryRef="PTCustomActions" />
|
||||||
|
|
||||||
<CustomAction Id="InstallCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallCmdPalPackageCA" BinaryRef="PTCustomActions" />
|
<CustomAction Id="InstallCmdPalPackage" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallCmdPalPackageCA" BinaryRef="PTCustomActions" />
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
|||||||
|
|
||||||
auto state = UpdateState::read();
|
auto state = UpdateState::read();
|
||||||
|
|
||||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
const auto new_version_info = get_github_version_info_async().get();
|
||||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||||
{
|
{
|
||||||
isUpToDate = true;
|
isUpToDate = true;
|
||||||
@@ -76,7 +76,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
|||||||
// Cleanup old updates before downloading the latest
|
// Cleanup old updates before downloading the latest
|
||||||
updating::cleanup_updates();
|
updating::cleanup_updates();
|
||||||
|
|
||||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
auto downloaded_installer = download_new_version(std::get<new_version_download_info>(*new_version_info)).get();
|
||||||
if (!downloaded_installer)
|
if (!downloaded_installer)
|
||||||
{
|
{
|
||||||
Logger::error("Couldn't download new installer");
|
Logger::error("Couldn't download new installer");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace // Strings in this namespace should not be localized
|
|||||||
|
|
||||||
namespace updating
|
namespace updating
|
||||||
{
|
{
|
||||||
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async()
|
std::future<bool> uninstall_previous_msix_version_async()
|
||||||
{
|
{
|
||||||
winrt::Windows::Management::Deployment::PackageManager package_manager;
|
winrt::Windows::Management::Deployment::PackageManager package_manager;
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
#include <winrt/Windows.Foundation.h>
|
|
||||||
#include <common/version/helper.h>
|
#include <common/version/helper.h>
|
||||||
|
|
||||||
namespace updating
|
namespace updating
|
||||||
{
|
{
|
||||||
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async();
|
std::future<bool> uninstall_previous_msix_version_async();
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
#include <winrt/Windows.System.h>
|
#include <winrt/Windows.System.h>
|
||||||
|
|
||||||
#include <wil/resource.h>
|
#include <wil/resource.h>
|
||||||
#include <wil/coroutine.h>
|
|
||||||
|
|
||||||
#endif //PCH_H
|
#endif //PCH_H
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ namespace updating
|
|||||||
// prevent the warning that may show up depend on the value of the constants (#defines)
|
// prevent the warning that may show up depend on the value of the constants (#defines)
|
||||||
#pragma warning(push)
|
#pragma warning(push)
|
||||||
#pragma warning(disable : 4702)
|
#pragma warning(disable : 4702)
|
||||||
wil::task<github_version_result> get_github_version_info_async(const bool prerelease)
|
#if USE_STD_EXPECTED
|
||||||
|
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
|
||||||
|
#else
|
||||||
|
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
|
// If 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 constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
|
||||||
@@ -166,7 +170,7 @@ namespace updating
|
|||||||
return !ec ? std::optional{ installer_download_path } : std::nullopt;
|
return !ec ? std::optional{ installer_download_path } : std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version)
|
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version)
|
||||||
{
|
{
|
||||||
auto installer_download_path = create_download_path();
|
auto installer_download_path = create_download_path();
|
||||||
if (!installer_download_path)
|
if (!installer_download_path)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <future>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
#include <winrt/Windows.Foundation.h>
|
#include <winrt/Windows.Foundation.h>
|
||||||
@@ -15,7 +16,6 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <common/version/helper.h>
|
#include <common/version/helper.h>
|
||||||
#include <wil/coroutine.h>
|
|
||||||
|
|
||||||
namespace updating
|
namespace updating
|
||||||
{
|
{
|
||||||
@@ -32,15 +32,13 @@ namespace updating
|
|||||||
};
|
};
|
||||||
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
|
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
|
||||||
|
|
||||||
#if USE_STD_EXPECTED
|
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version);
|
||||||
using github_version_result = std::expected<github_version_info, std::wstring>;
|
|
||||||
#else
|
|
||||||
using github_version_result = nonstd::expected<github_version_info, std::wstring>;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
wil::task<github_version_result> get_github_version_info_async(bool prerelease = false);
|
|
||||||
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version);
|
|
||||||
std::filesystem::path get_pending_updates_path();
|
std::filesystem::path get_pending_updates_path();
|
||||||
|
#if USE_STD_EXPECTED
|
||||||
|
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
|
||||||
|
#else
|
||||||
|
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
|
||||||
|
#endif
|
||||||
void cleanup_updates();
|
void cleanup_updates();
|
||||||
|
|
||||||
// non-localized
|
// non-localized
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
#include <future>
|
||||||
#include <string>
|
|
||||||
#include <winrt/Windows.Foundation.h>
|
#include <winrt/Windows.Foundation.h>
|
||||||
#include <winrt/Windows.Storage.Streams.h>
|
#include <winrt/Windows.Storage.Streams.h>
|
||||||
#include <winrt/Windows.Web.Http.h>
|
#include <winrt/Windows.Web.Http.h>
|
||||||
@@ -22,15 +21,15 @@ namespace http
|
|||||||
headers.UserAgent().TryParseAdd(USER_AGENT);
|
headers.UserAgent().TryParseAdd(USER_AGENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> request(winrt::Windows::Foundation::Uri url)
|
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
|
||||||
{
|
{
|
||||||
auto response = co_await m_client.GetAsync(url);
|
auto response = co_await m_client.GetAsync(url);
|
||||||
(void)response.EnsureSuccessStatusCode();
|
(void)response.EnsureSuccessStatusCode();
|
||||||
auto body = co_await response.Content().ReadAsStringAsync();
|
auto body = co_await response.Content().ReadAsStringAsync();
|
||||||
co_return body;
|
co_return std::wstring(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath)
|
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
|
||||||
{
|
{
|
||||||
auto response = co_await m_client.GetAsync(url);
|
auto response = co_await m_client.GetAsync(url);
|
||||||
(void)response.EnsureSuccessStatusCode();
|
(void)response.EnsureSuccessStatusCode();
|
||||||
@@ -39,7 +38,7 @@ namespace http
|
|||||||
file_stream.Close();
|
file_stream.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath, std::function<void(float)> progressUpdateCallback)
|
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
|
||||||
{
|
{
|
||||||
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<ConformanceMode>false</ConformanceMode>
|
<ConformanceMode>false</ConformanceMode>
|
||||||
<TreatWarningAsError>true</TreatWarningAsError>
|
<TreatWarningAsError>true</TreatWarningAsError>
|
||||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||||
|
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<ConformanceMode>false</ConformanceMode>
|
<ConformanceMode>false</ConformanceMode>
|
||||||
<TreatWarningAsError>true</TreatWarningAsError>
|
<TreatWarningAsError>true</TreatWarningAsError>
|
||||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||||
|
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<ConformanceMode>false</ConformanceMode>
|
<ConformanceMode>false</ConformanceMode>
|
||||||
<TreatWarningAsError>true</TreatWarningAsError>
|
<TreatWarningAsError>true</TreatWarningAsError>
|
||||||
<LanguageStandard>stdcpplatest</LanguageStandard>
|
<LanguageStandard>stdcpplatest</LanguageStandard>
|
||||||
|
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
|
||||||
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
|
|||||||
@@ -5140,7 +5140,7 @@ bool IsPenInverted( WPARAM wParam )
|
|||||||
// Captures the specified screen using the capture APIs
|
// Captures the specified screen using the capture APIs
|
||||||
//
|
//
|
||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
wil::task<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
|
std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
|
||||||
{
|
{
|
||||||
auto d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(device);
|
auto d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(device);
|
||||||
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
|
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
|
||||||
@@ -5176,7 +5176,9 @@ wil::task<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect
|
|||||||
framePool.Close();
|
framePool.Close();
|
||||||
|
|
||||||
auto texture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
auto texture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
||||||
co_return util::CopyD3DTexture(d3dDevice, texture, true);
|
auto result = util::CopyD3DTexture(d3dDevice, texture, true);
|
||||||
|
|
||||||
|
co_return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
@@ -5203,7 +5205,10 @@ winrt::com_ptr<ID3D11Texture2D>CaptureScreenshot(winrt::DirectXPixelFormat const
|
|||||||
|
|
||||||
auto item = util::CreateCaptureItemForMonitor(hMon);
|
auto item = util::CreateCaptureItemForMonitor(hMon);
|
||||||
|
|
||||||
return CaptureScreenshotAsync(device, item, pixelFormat).get();
|
auto capture = CaptureScreenshotAsync(device, item, pixelFormat);
|
||||||
|
capture.wait();
|
||||||
|
|
||||||
|
return capture.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,6 @@
|
|||||||
// WIL
|
// WIL
|
||||||
#include <wil/com.h>
|
#include <wil/com.h>
|
||||||
#include <wil/resource.h>
|
#include <wil/resource.h>
|
||||||
#include <wil/coroutine.h>
|
|
||||||
|
|
||||||
// DirectX
|
// DirectX
|
||||||
#include <d3d11_4.h>
|
#include <d3d11_4.h>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the packaging flavor of the application.
|
||||||
|
/// </summary>
|
||||||
|
public enum AppPackagingFlavor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Application is packaged as a Windows MSIX package.
|
||||||
|
/// </summary>
|
||||||
|
Packaged,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application is running unpackaged (native executable).
|
||||||
|
/// </summary>
|
||||||
|
Unpackaged,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application is running as unpackaged portable (self-contained distribution).
|
||||||
|
/// </summary>
|
||||||
|
UnpackagedPortable,
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPrecomputedListItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the fuzzy matching target for the item's title.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
|
||||||
|
/// <returns>The fuzzy target for the title.</returns>
|
||||||
|
FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the fuzzy matching target for the item's subtitle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
|
||||||
|
/// <returns>The fuzzy target for the subtitle.</returns>
|
||||||
|
FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
|
|
||||||
|
public static partial class InternalListHelpers
|
||||||
|
{
|
||||||
|
public static RoScored<T>[] FilterListWithScores<T>(
|
||||||
|
IEnumerable<T>? items,
|
||||||
|
in FuzzyQuery query,
|
||||||
|
in ScoringFunction<T> scoreFunction)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get initial capacity hint
|
||||||
|
var initialCapacity = items switch
|
||||||
|
{
|
||||||
|
ICollection<T> col => col.Count,
|
||||||
|
IReadOnlyCollection<T> rc => rc.Count,
|
||||||
|
_ => 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var score = scoreFunction(in query, item);
|
||||||
|
if (score <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == buffer.Length)
|
||||||
|
{
|
||||||
|
GrowBuffer(ref buffer, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[count++] = new RoScored<T>(item, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
|
||||||
|
var result = GC.AllocateUninitializedArray<RoScored<T>>(count);
|
||||||
|
buffer.AsSpan(0, count).CopyTo(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GrowBuffer<T>(ref RoScored<T>[] buffer, int count)
|
||||||
|
{
|
||||||
|
var newBuffer = ArrayPool<RoScored<T>>.Shared.Rent(buffer.Length * 2);
|
||||||
|
buffer.AsSpan(0, count).CopyTo(newBuffer);
|
||||||
|
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||||
|
buffer = newBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T[] FilterList<T>(IEnumerable<T> items, in FuzzyQuery query, ScoringFunction<T> scoreFunction)
|
||||||
|
{
|
||||||
|
// Try to get initial capacity hint
|
||||||
|
var initialCapacity = items switch
|
||||||
|
{
|
||||||
|
ICollection<T> col => col.Count,
|
||||||
|
IReadOnlyCollection<T> rc => rc.Count,
|
||||||
|
_ => 64,
|
||||||
|
};
|
||||||
|
|
||||||
|
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var score = scoreFunction(in query, item);
|
||||||
|
if (score <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == buffer.Length)
|
||||||
|
{
|
||||||
|
GrowBuffer(ref buffer, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[count++] = new RoScored<T>(item, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
|
||||||
|
|
||||||
|
var result = GC.AllocateUninitializedArray<T>(count);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
result[i] = buffer[i].Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<RoScored<T>>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct RoScoredDescendingComparer<T> : IComparer<RoScored<T>>
|
||||||
|
{
|
||||||
|
public int Compare(RoScored<T> x, RoScored<T> y) => y.Score.CompareTo(x.Score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate int ScoringFunction<in T>(in FuzzyQuery query, T item);
|
||||||
|
|
||||||
|
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
|
||||||
|
public readonly struct RoScored<T>
|
||||||
|
{
|
||||||
|
public readonly int Score;
|
||||||
|
public readonly T Item;
|
||||||
|
|
||||||
|
public RoScored(T item, int score)
|
||||||
|
{
|
||||||
|
Score = score;
|
||||||
|
Item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDebuggerDisplay()
|
||||||
|
{
|
||||||
|
return "Score = " + Score + ", Item = " + Item;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
|
||||||
|
/// </summary>
|
||||||
|
public interface IApplicationInfoService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
|
||||||
|
/// </summary>
|
||||||
|
string AppVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the packaging flavor of the application.
|
||||||
|
/// </summary>
|
||||||
|
AppPackagingFlavor PackagingFlavor { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the directory path where application logs are stored.
|
||||||
|
/// </summary>
|
||||||
|
string LogDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the directory path where application configuration files are stored.
|
||||||
|
/// </summary>
|
||||||
|
string ConfigDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the application is running with administrator privileges.
|
||||||
|
/// </summary>
|
||||||
|
bool IsElevated { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a formatted summary of application information suitable for logging.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A formatted string containing application information.</returns>
|
||||||
|
string GetApplicationInfoSummary();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the log directory delegate to be used for retrieving the log directory path.
|
||||||
|
/// This allows deferred initialization of the logger path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
|
||||||
|
void SetLogDirectory(Func<string> getLogDirectory);
|
||||||
|
}
|
||||||
@@ -2,20 +2,27 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Principal;
|
|
||||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
|
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||||
|
|
||||||
public sealed class ErrorReportBuilder : IErrorReportBuilder
|
public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||||
{
|
{
|
||||||
private readonly ErrorReportSanitizer _sanitizer = new();
|
private readonly ErrorReportSanitizer _sanitizer = new();
|
||||||
|
private readonly IApplicationInfoService _appInfoService;
|
||||||
|
|
||||||
private static string Preamble => Properties.Resources.ErrorReport_Global_Preamble;
|
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)
|
public string BuildReport(Exception exception, string context, bool redactPii = true)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(exception);
|
ArgumentNullException.ThrowIfNull(exception);
|
||||||
@@ -24,6 +31,9 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
|||||||
var sanitizedMessage = redactPii ? _sanitizer.Sanitize(exceptionMessage) : exceptionMessage;
|
var sanitizedMessage = redactPii ? _sanitizer.Sanitize(exceptionMessage) : exceptionMessage;
|
||||||
var sanitizedFormattedException = redactPii ? _sanitizer.Sanitize(exception.ToString()) : exception.ToString();
|
var sanitizedFormattedException = redactPii ? _sanitizer.Sanitize(exception.ToString()) : exception.ToString();
|
||||||
|
|
||||||
|
var applicationInfoSummary = GetAppInfoSafe();
|
||||||
|
var applicationInfoSummarySanitized = redactPii ? _sanitizer.Sanitize(applicationInfoSummary) : applicationInfoSummary;
|
||||||
|
|
||||||
// Note:
|
// Note:
|
||||||
// - do not localize technical part of the report, we need to ensure it can be read by developers
|
// - 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)
|
// - keep timestamp format should be consistent with the log (makes it easier to search)
|
||||||
@@ -38,18 +48,7 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
|||||||
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
|
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
|
||||||
Context: {context ?? "N/A"}
|
Context: {context ?? "N/A"}
|
||||||
|
|
||||||
Application:
|
{applicationInfoSummarySanitized}
|
||||||
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:
|
Stack Trace:
|
||||||
{exception.StackTrace}
|
{exception.StackTrace}
|
||||||
@@ -66,31 +65,17 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
|
|||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetElevationStatus()
|
private string? GetAppInfoSafe()
|
||||||
{
|
{
|
||||||
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
return _appInfoService.GetApplicationInfoSummary();
|
||||||
return isElevated ? "yes" : "no";
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return "Failed to determine elevation status";
|
// 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}";
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class BloomFilter : IBloomFilter
|
||||||
|
{
|
||||||
|
public ulong Compute(string input)
|
||||||
|
{
|
||||||
|
ulong bloom = 0;
|
||||||
|
|
||||||
|
foreach (var ch in input)
|
||||||
|
{
|
||||||
|
if (SymbolClassifier.Classify(ch) == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var h = (uint)ch * 0x45d9f3b;
|
||||||
|
bloom |= 1UL << (int)(h & 31);
|
||||||
|
bloom |= 1UL << (int)(((h >> 16) & 31) + 32);
|
||||||
|
|
||||||
|
if (bloom == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bloom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool MightContain(ulong candidateBloom, ulong queryBloom)
|
||||||
|
{
|
||||||
|
return (candidateBloom & queryBloom) == queryBloom;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class FuzzyMatcherProvider : IFuzzyMatcherProvider
|
||||||
|
{
|
||||||
|
private readonly IBloomFilter _bloomCalculator = new BloomFilter();
|
||||||
|
private readonly IStringFolder _normalizer = new StringFolder();
|
||||||
|
|
||||||
|
private IPrecomputedFuzzyMatcher _current;
|
||||||
|
|
||||||
|
public FuzzyMatcherProvider(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
|
||||||
|
{
|
||||||
|
_current = CreateMatcher(core, pinyin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPrecomputedFuzzyMatcher Current => Volatile.Read(ref _current);
|
||||||
|
|
||||||
|
public void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _current, CreateMatcher(core, pinyin));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IPrecomputedFuzzyMatcher CreateMatcher(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin)
|
||||||
|
{
|
||||||
|
return pinyin is null || !IsPinyinEnabled(pinyin)
|
||||||
|
? new PrecomputedFuzzyMatcher(core, _normalizer, _bloomCalculator)
|
||||||
|
: new PrecomputedFuzzyMatcherWithPinyin(core, pinyin, _normalizer, _bloomCalculator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o)
|
||||||
|
{
|
||||||
|
return o.Mode switch
|
||||||
|
{
|
||||||
|
PinyinMode.Off => false,
|
||||||
|
PinyinMode.On => true,
|
||||||
|
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSimplifiedChineseUi()
|
||||||
|
{
|
||||||
|
var culture = CultureInfo.CurrentUICulture;
|
||||||
|
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public readonly struct FuzzyQuery
|
||||||
|
{
|
||||||
|
public readonly string Original;
|
||||||
|
|
||||||
|
public readonly string Folded;
|
||||||
|
|
||||||
|
public readonly ulong Bloom;
|
||||||
|
|
||||||
|
public readonly int EffectiveLength;
|
||||||
|
|
||||||
|
public readonly bool IsAllLowercaseAsciiOrNonLetter;
|
||||||
|
|
||||||
|
public readonly string? SecondaryOriginal;
|
||||||
|
|
||||||
|
public readonly string? SecondaryFolded;
|
||||||
|
|
||||||
|
public readonly ulong SecondaryBloom;
|
||||||
|
|
||||||
|
public readonly int SecondaryEffectiveLength;
|
||||||
|
|
||||||
|
public readonly bool SecondaryIsAllLowercaseAsciiOrNonLetter;
|
||||||
|
|
||||||
|
public int Length => Folded.Length;
|
||||||
|
|
||||||
|
public bool HasSecondary => SecondaryFolded is not null;
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
|
||||||
|
|
||||||
|
public FuzzyQuery(
|
||||||
|
string original,
|
||||||
|
string folded,
|
||||||
|
ulong bloom,
|
||||||
|
int effectiveLength,
|
||||||
|
bool isAllLowercaseAsciiOrNonLetter,
|
||||||
|
string? secondaryOriginal = null,
|
||||||
|
string? secondaryFolded = null,
|
||||||
|
ulong secondaryBloom = 0,
|
||||||
|
int secondaryEffectiveLength = 0,
|
||||||
|
bool secondaryIsAllLowercaseAsciiOrNonLetter = true)
|
||||||
|
{
|
||||||
|
Original = original;
|
||||||
|
Folded = folded;
|
||||||
|
Bloom = bloom;
|
||||||
|
EffectiveLength = effectiveLength;
|
||||||
|
IsAllLowercaseAsciiOrNonLetter = isAllLowercaseAsciiOrNonLetter;
|
||||||
|
|
||||||
|
SecondaryOriginal = secondaryOriginal;
|
||||||
|
SecondaryFolded = secondaryFolded;
|
||||||
|
SecondaryBloom = secondaryBloom;
|
||||||
|
SecondaryEffectiveLength = secondaryEffectiveLength;
|
||||||
|
SecondaryIsAllLowercaseAsciiOrNonLetter = secondaryIsAllLowercaseAsciiOrNonLetter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public readonly struct FuzzyTarget
|
||||||
|
{
|
||||||
|
public readonly string Original;
|
||||||
|
public readonly string Folded;
|
||||||
|
public readonly ulong Bloom;
|
||||||
|
|
||||||
|
public readonly string? SecondaryOriginal;
|
||||||
|
public readonly string? SecondaryFolded;
|
||||||
|
public readonly ulong SecondaryBloom;
|
||||||
|
|
||||||
|
public int Length => Folded.Length;
|
||||||
|
|
||||||
|
public bool HasSecondary => SecondaryFolded is not null;
|
||||||
|
|
||||||
|
public int SecondaryLength => SecondaryFolded?.Length ?? 0;
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
|
||||||
|
|
||||||
|
public FuzzyTarget(
|
||||||
|
string original,
|
||||||
|
string folded,
|
||||||
|
ulong bloom,
|
||||||
|
string? secondaryOriginal = null,
|
||||||
|
string? secondaryFolded = null,
|
||||||
|
ulong secondaryBloom = 0)
|
||||||
|
{
|
||||||
|
Original = original;
|
||||||
|
Folded = folded;
|
||||||
|
Bloom = bloom;
|
||||||
|
SecondaryOriginal = secondaryOriginal;
|
||||||
|
SecondaryFolded = secondaryFolded;
|
||||||
|
SecondaryBloom = secondaryBloom;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public struct FuzzyTargetCache
|
||||||
|
{
|
||||||
|
private string? _lastRaw;
|
||||||
|
private uint _schemaId;
|
||||||
|
private FuzzyTarget _target;
|
||||||
|
|
||||||
|
public FuzzyTarget GetOrUpdate(IPrecomputedFuzzyMatcher matcher, string? raw)
|
||||||
|
{
|
||||||
|
raw ??= string.Empty;
|
||||||
|
|
||||||
|
if (_schemaId == matcher.SchemaId && string.Equals(_lastRaw, raw, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _target;
|
||||||
|
}
|
||||||
|
|
||||||
|
_target = matcher.PrecomputeTarget(raw);
|
||||||
|
_schemaId = matcher.SchemaId;
|
||||||
|
_lastRaw = raw;
|
||||||
|
return _target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
_lastRaw = null;
|
||||||
|
_target = default;
|
||||||
|
_schemaId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public interface IBloomFilter
|
||||||
|
{
|
||||||
|
ulong Compute(string input);
|
||||||
|
|
||||||
|
bool MightContain(ulong candidateBloom, ulong queryBloom);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public interface IFuzzyMatcherProvider
|
||||||
|
{
|
||||||
|
IPrecomputedFuzzyMatcher Current { get; }
|
||||||
|
|
||||||
|
void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public interface IPrecomputedFuzzyMatcher
|
||||||
|
{
|
||||||
|
uint SchemaId { get; }
|
||||||
|
|
||||||
|
FuzzyQuery PrecomputeQuery(string? input);
|
||||||
|
|
||||||
|
FuzzyTarget PrecomputeTarget(string? input);
|
||||||
|
|
||||||
|
int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public interface IStringFolder
|
||||||
|
{
|
||||||
|
string Fold(string input, bool removeDiacritics);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class PinyinFuzzyMatcherOptions
|
||||||
|
{
|
||||||
|
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
|
||||||
|
|
||||||
|
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
|
||||||
|
public bool RemoveApostrophesForQuery { get; init; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public enum PinyinMode
|
||||||
|
{
|
||||||
|
Off = 0,
|
||||||
|
AutoSimplifiedChineseUi = 1,
|
||||||
|
On = 2,
|
||||||
|
}
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class PrecomputedFuzzyMatcher : IPrecomputedFuzzyMatcher
|
||||||
|
{
|
||||||
|
private const int NoMatchScore = 0;
|
||||||
|
private const int StackallocThresholdChars = 512;
|
||||||
|
private const int FolderSchemaVersion = 1;
|
||||||
|
private const int BloomSchemaVersion = 1;
|
||||||
|
|
||||||
|
private readonly PrecomputedFuzzyMatcherOptions _options;
|
||||||
|
private readonly IStringFolder _stringFolder;
|
||||||
|
private readonly IBloomFilter _bloom;
|
||||||
|
|
||||||
|
public PrecomputedFuzzyMatcher(
|
||||||
|
PrecomputedFuzzyMatcherOptions? options = null,
|
||||||
|
IStringFolder? normalization = null,
|
||||||
|
IBloomFilter? bloomCalculator = null)
|
||||||
|
{
|
||||||
|
_options = options ?? PrecomputedFuzzyMatcherOptions.Default;
|
||||||
|
_bloom = bloomCalculator ?? new BloomFilter();
|
||||||
|
_stringFolder = normalization ?? new StringFolder();
|
||||||
|
|
||||||
|
SchemaId = ComputeSchemaId(_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint SchemaId { get; }
|
||||||
|
|
||||||
|
public FuzzyQuery PrecomputeQuery(string? input) => PrecomputeQuery(input, null);
|
||||||
|
|
||||||
|
public FuzzyTarget PrecomputeTarget(string? input) => PrecomputeTarget(input, null);
|
||||||
|
|
||||||
|
public int Score(in FuzzyQuery query, in FuzzyTarget target)
|
||||||
|
{
|
||||||
|
var qFold = query.FoldedSpan;
|
||||||
|
var tLen = target.Length;
|
||||||
|
|
||||||
|
if (query.EffectiveLength == 0 || tLen == 0)
|
||||||
|
{
|
||||||
|
return NoMatchScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipWordSeparators = _options.SkipWordSeparators;
|
||||||
|
var bestScore = 0;
|
||||||
|
|
||||||
|
// 1. Primary → Primary
|
||||||
|
if (tLen >= query.EffectiveLength && _bloom.MightContain(target.Bloom, query.Bloom))
|
||||||
|
{
|
||||||
|
if (CanMatchSubsequence(qFold, target.FoldedSpan, skipWordSeparators))
|
||||||
|
{
|
||||||
|
bestScore = ScoreNonContiguous(
|
||||||
|
qRaw: query.OriginalSpan,
|
||||||
|
qFold: qFold,
|
||||||
|
qEffectiveLen: query.EffectiveLength,
|
||||||
|
tRaw: target.OriginalSpan,
|
||||||
|
tFold: target.FoldedSpan,
|
||||||
|
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Secondary → Secondary
|
||||||
|
if (query.HasSecondary && target.HasSecondary)
|
||||||
|
{
|
||||||
|
var qSecFold = query.SecondaryFoldedSpan;
|
||||||
|
|
||||||
|
if (target.SecondaryLength >= query.SecondaryEffectiveLength &&
|
||||||
|
_bloom.MightContain(target.SecondaryBloom, query.SecondaryBloom) &&
|
||||||
|
CanMatchSubsequence(qSecFold, target.SecondaryFoldedSpan, skipWordSeparators))
|
||||||
|
{
|
||||||
|
var score = ScoreNonContiguous(
|
||||||
|
qRaw: query.SecondaryOriginalSpan,
|
||||||
|
qFold: qSecFold,
|
||||||
|
qEffectiveLen: query.SecondaryEffectiveLength,
|
||||||
|
tRaw: target.SecondaryOriginalSpan,
|
||||||
|
tFold: target.SecondaryFoldedSpan,
|
||||||
|
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||||
|
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Primary query → Secondary target
|
||||||
|
if (target.HasSecondary &&
|
||||||
|
target.SecondaryLength >= query.EffectiveLength &&
|
||||||
|
_bloom.MightContain(target.SecondaryBloom, query.Bloom))
|
||||||
|
{
|
||||||
|
if (CanMatchSubsequence(qFold, target.SecondaryFoldedSpan, skipWordSeparators))
|
||||||
|
{
|
||||||
|
var score = ScoreNonContiguous(
|
||||||
|
qRaw: query.OriginalSpan,
|
||||||
|
qFold: qFold,
|
||||||
|
qEffectiveLen: query.EffectiveLength,
|
||||||
|
tRaw: target.SecondaryOriginalSpan,
|
||||||
|
tFold: target.SecondaryFoldedSpan,
|
||||||
|
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
|
||||||
|
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Secondary query → Primary target
|
||||||
|
if (query.HasSecondary &&
|
||||||
|
tLen >= query.SecondaryEffectiveLength &&
|
||||||
|
_bloom.MightContain(target.Bloom, query.SecondaryBloom))
|
||||||
|
{
|
||||||
|
var qSecFold = query.SecondaryFoldedSpan;
|
||||||
|
|
||||||
|
if (CanMatchSubsequence(qSecFold, target.FoldedSpan, skipWordSeparators))
|
||||||
|
{
|
||||||
|
var score = ScoreNonContiguous(
|
||||||
|
qRaw: query.SecondaryOriginalSpan,
|
||||||
|
qFold: qSecFold,
|
||||||
|
qEffectiveLen: query.SecondaryEffectiveLength,
|
||||||
|
tRaw: target.OriginalSpan,
|
||||||
|
tFold: target.FoldedSpan,
|
||||||
|
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||||
|
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FuzzyQuery PrecomputeQuery(string? input, string? secondaryInput)
|
||||||
|
{
|
||||||
|
input ??= string.Empty;
|
||||||
|
|
||||||
|
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
|
||||||
|
var bloom = _bloom.Compute(folded);
|
||||||
|
var effectiveLength = _options.SkipWordSeparators
|
||||||
|
? folded.Length - CountWordSeparators(folded)
|
||||||
|
: folded.Length;
|
||||||
|
|
||||||
|
var isAllLowercase = IsAllLowercaseAsciiOrNonLetter(input);
|
||||||
|
|
||||||
|
string? secondaryOriginal = null;
|
||||||
|
string? secondaryFolded = null;
|
||||||
|
ulong secondaryBloom = 0;
|
||||||
|
var secondaryEffectiveLength = 0;
|
||||||
|
var secondaryIsAllLowercase = true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(secondaryInput))
|
||||||
|
{
|
||||||
|
secondaryOriginal = secondaryInput;
|
||||||
|
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
|
||||||
|
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||||
|
secondaryEffectiveLength = _options.SkipWordSeparators
|
||||||
|
? secondaryFolded.Length - CountWordSeparators(secondaryFolded)
|
||||||
|
: secondaryFolded.Length;
|
||||||
|
|
||||||
|
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondaryInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FuzzyQuery(
|
||||||
|
original: input,
|
||||||
|
folded: folded,
|
||||||
|
bloom: bloom,
|
||||||
|
effectiveLength: effectiveLength,
|
||||||
|
isAllLowercaseAsciiOrNonLetter: isAllLowercase,
|
||||||
|
secondaryOriginal: secondaryOriginal,
|
||||||
|
secondaryFolded: secondaryFolded,
|
||||||
|
secondaryBloom: secondaryBloom,
|
||||||
|
secondaryEffectiveLength: secondaryEffectiveLength,
|
||||||
|
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
static int CountWordSeparators(string s)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
foreach (var c in s)
|
||||||
|
{
|
||||||
|
if (SymbolClassifier.Classify(c) == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal FuzzyTarget PrecomputeTarget(string? input, string? secondaryInput)
|
||||||
|
{
|
||||||
|
input ??= string.Empty;
|
||||||
|
|
||||||
|
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
|
||||||
|
var bloom = _bloom.Compute(folded);
|
||||||
|
|
||||||
|
string? secondaryFolded = null;
|
||||||
|
ulong secondaryBloom = 0;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(secondaryInput))
|
||||||
|
{
|
||||||
|
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
|
||||||
|
secondaryBloom = _bloom.Compute(secondaryFolded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FuzzyTarget(
|
||||||
|
input,
|
||||||
|
folded,
|
||||||
|
bloom,
|
||||||
|
secondaryInput,
|
||||||
|
secondaryFolded,
|
||||||
|
secondaryBloom);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
|
||||||
|
{
|
||||||
|
foreach (var c in s)
|
||||||
|
{
|
||||||
|
if ((uint)(c - 'A') <= ('Z' - 'A'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool CanMatchSubsequence(
|
||||||
|
ReadOnlySpan<char> qFold,
|
||||||
|
ReadOnlySpan<char> tFold,
|
||||||
|
bool skipWordSeparators)
|
||||||
|
{
|
||||||
|
var qi = 0;
|
||||||
|
var ti = 0;
|
||||||
|
|
||||||
|
while (qi < qFold.Length && ti < tFold.Length)
|
||||||
|
{
|
||||||
|
var qChar = qFold[qi];
|
||||||
|
|
||||||
|
if (skipWordSeparators && SymbolClassifier.Classify(qChar) == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
qi++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qChar == tFold[ti])
|
||||||
|
{
|
||||||
|
qi++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ti++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip trailing word separators in query
|
||||||
|
if (skipWordSeparators)
|
||||||
|
{
|
||||||
|
while (qi < qFold.Length && SymbolClassifier.Classify(qFold[qi]) == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
qi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return qi == qFold.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||||
|
[SkipLocalsInit]
|
||||||
|
private int ScoreNonContiguous(
|
||||||
|
scoped in ReadOnlySpan<char> qRaw,
|
||||||
|
scoped in ReadOnlySpan<char> qFold,
|
||||||
|
int qEffectiveLen,
|
||||||
|
scoped in ReadOnlySpan<char> tRaw,
|
||||||
|
scoped in ReadOnlySpan<char> tFold,
|
||||||
|
bool ignoreSameCaseBonusForThisQuery)
|
||||||
|
{
|
||||||
|
Debug.Assert(qRaw.Length == qFold.Length, "Original and folded spans are traversed in lockstep: requires qRaw.Length == qFold.Length");
|
||||||
|
Debug.Assert(tRaw.Length == tFold.Length, "Original and folded spans are traversed in lockstep: requires tRaw.Length == tFold.Length");
|
||||||
|
Debug.Assert(qEffectiveLen <= qFold.Length, "Effective length must be less than or equal to folded length");
|
||||||
|
|
||||||
|
var qLen = qFold.Length;
|
||||||
|
var tLen = tFold.Length;
|
||||||
|
|
||||||
|
// Copy options to local variables to avoid repeated field accesses
|
||||||
|
var charMatchBonus = _options.CharMatchBonus;
|
||||||
|
var sameCaseBonus = ignoreSameCaseBonusForThisQuery ? 0 : _options.SameCaseBonus;
|
||||||
|
var consecutiveMultiplier = _options.ConsecutiveMultiplier;
|
||||||
|
var camelCaseBonus = _options.CamelCaseBonus;
|
||||||
|
var startOfWordBonus = _options.StartOfWordBonus;
|
||||||
|
var pathSeparatorBonus = _options.PathSeparatorBonus;
|
||||||
|
var wordSeparatorBonus = _options.WordSeparatorBonus;
|
||||||
|
var separatorAlignmentBonus = _options.SeparatorAlignmentBonus;
|
||||||
|
var exactSeparatorBonus = _options.ExactSeparatorBonus;
|
||||||
|
var skipWordSeparators = _options.SkipWordSeparators;
|
||||||
|
|
||||||
|
// DP buffer: two rows of length tLen
|
||||||
|
var bufferSize = tLen * 2;
|
||||||
|
int[]? rented = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scoped Span<int> buffer;
|
||||||
|
if (bufferSize <= StackallocThresholdChars)
|
||||||
|
{
|
||||||
|
buffer = stackalloc int[bufferSize];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rented = ArrayPool<int>.Shared.Rent(bufferSize);
|
||||||
|
buffer = rented.AsSpan(0, bufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scores = buffer[..tLen];
|
||||||
|
var seqLens = buffer.Slice(tLen, tLen);
|
||||||
|
|
||||||
|
scores.Clear();
|
||||||
|
seqLens.Clear();
|
||||||
|
|
||||||
|
ref var scores0 = ref MemoryMarshal.GetReference(scores);
|
||||||
|
ref var seqLens0 = ref MemoryMarshal.GetReference(seqLens);
|
||||||
|
ref var qRaw0 = ref MemoryMarshal.GetReference(qRaw);
|
||||||
|
ref var qFold0 = ref MemoryMarshal.GetReference(qFold);
|
||||||
|
ref var tRaw0 = ref MemoryMarshal.GetReference(tRaw);
|
||||||
|
ref var tFold0 = ref MemoryMarshal.GetReference(tFold);
|
||||||
|
|
||||||
|
var qiEffective = 0;
|
||||||
|
|
||||||
|
for (var qi = 0; qi < qLen; qi++)
|
||||||
|
{
|
||||||
|
var qCharFold = Unsafe.Add(ref qFold0, qi);
|
||||||
|
var qCharKind = SymbolClassifier.Classify(qCharFold);
|
||||||
|
|
||||||
|
if (skipWordSeparators && qCharKind == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoisted values
|
||||||
|
var qRawIsUpper = char.IsUpper(Unsafe.Add(ref qRaw0, qi));
|
||||||
|
|
||||||
|
// row computation
|
||||||
|
var leftScore = 0;
|
||||||
|
var diagScore = 0;
|
||||||
|
var diagSeqLen = 0;
|
||||||
|
|
||||||
|
// limit ti to ensure enough remaining characters to match the rest of the query
|
||||||
|
var tiMax = tLen - qEffectiveLen + qiEffective;
|
||||||
|
|
||||||
|
for (var ti = 0; ti <= tiMax; ti++)
|
||||||
|
{
|
||||||
|
var upScore = Unsafe.Add(ref scores0, ti);
|
||||||
|
var upSeqLen = Unsafe.Add(ref seqLens0, ti);
|
||||||
|
|
||||||
|
var charScore = 0;
|
||||||
|
if (diagScore != 0 || qiEffective == 0)
|
||||||
|
{
|
||||||
|
charScore = ComputeCharScore(
|
||||||
|
qi,
|
||||||
|
ti,
|
||||||
|
qCharFold,
|
||||||
|
qCharKind,
|
||||||
|
diagSeqLen,
|
||||||
|
qRawIsUpper,
|
||||||
|
ref tRaw0,
|
||||||
|
ref qFold0,
|
||||||
|
ref tFold0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateScore = diagScore + charScore;
|
||||||
|
if (charScore != 0 && candidateScore >= leftScore)
|
||||||
|
{
|
||||||
|
Unsafe.Add(ref scores0, ti) = candidateScore;
|
||||||
|
Unsafe.Add(ref seqLens0, ti) = diagSeqLen + 1;
|
||||||
|
leftScore = candidateScore;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Unsafe.Add(ref scores0, ti) = leftScore;
|
||||||
|
Unsafe.Add(ref seqLens0, ti) = 0;
|
||||||
|
/* leftScore remains unchanged */
|
||||||
|
}
|
||||||
|
|
||||||
|
diagScore = upScore;
|
||||||
|
diagSeqLen = upSeqLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit: no match possible
|
||||||
|
if (leftScore == 0)
|
||||||
|
{
|
||||||
|
return NoMatchScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance effective query index
|
||||||
|
// Only counts non-separator characters if skipWordSeparators is enabled
|
||||||
|
qiEffective++;
|
||||||
|
|
||||||
|
if (qiEffective == qEffectiveLen)
|
||||||
|
{
|
||||||
|
return leftScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scores[tLen - 1];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
int ComputeCharScore(
|
||||||
|
int qi,
|
||||||
|
int ti,
|
||||||
|
char qCharFold,
|
||||||
|
SymbolKind qCharKind,
|
||||||
|
int seqLen,
|
||||||
|
bool qCharRawCurrIsUpper,
|
||||||
|
ref char tRaw0,
|
||||||
|
ref char qFold0,
|
||||||
|
ref char tFold0)
|
||||||
|
{
|
||||||
|
// Match check:
|
||||||
|
// - exact folded char match always ok
|
||||||
|
// - otherwise, allow equivalence only for word separators (e.g. '_' matches '-')
|
||||||
|
var tCharFold = Unsafe.Add(ref tFold0, ti);
|
||||||
|
if (qCharFold != tCharFold)
|
||||||
|
{
|
||||||
|
if (!skipWordSeparators)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qCharKind != SymbolKind.WordSeparator ||
|
||||||
|
SymbolClassifier.Classify(tCharFold) != SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0. Base char match bonus
|
||||||
|
var score = charMatchBonus;
|
||||||
|
|
||||||
|
// 1. Consecutive match bonus
|
||||||
|
if (seqLen > 0)
|
||||||
|
{
|
||||||
|
score += seqLen * consecutiveMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Same case bonus
|
||||||
|
// Early outs to appease the branch predictor
|
||||||
|
if (sameCaseBonus != 0)
|
||||||
|
{
|
||||||
|
var tCharRawCurr = Unsafe.Add(ref tRaw0, ti);
|
||||||
|
var tCharRawCurrIsUpper = char.IsUpper(tCharRawCurr);
|
||||||
|
if (qCharRawCurrIsUpper == tCharRawCurrIsUpper)
|
||||||
|
{
|
||||||
|
score += sameCaseBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ti == 0)
|
||||||
|
{
|
||||||
|
score += startOfWordBonus;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
|
||||||
|
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
|
||||||
|
if (tPrevKind != SymbolKind.Other)
|
||||||
|
{
|
||||||
|
score += tPrevKind == SymbolKind.PathSeparator
|
||||||
|
? pathSeparatorBonus
|
||||||
|
: wordSeparatorBonus;
|
||||||
|
|
||||||
|
if (skipWordSeparators && seqLen == 0 && qi > 0)
|
||||||
|
{
|
||||||
|
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
|
||||||
|
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
|
||||||
|
|
||||||
|
if (qPrevKind == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
score += separatorAlignmentBonus;
|
||||||
|
|
||||||
|
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
|
||||||
|
{
|
||||||
|
score += exactSeparatorBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tCharRawCurrIsUpper && seqLen == 0)
|
||||||
|
{
|
||||||
|
score += camelCaseBonus;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ti == 0)
|
||||||
|
{
|
||||||
|
score += startOfWordBonus;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
|
||||||
|
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
|
||||||
|
if (tPrevKind != SymbolKind.Other)
|
||||||
|
{
|
||||||
|
score += tPrevKind == SymbolKind.PathSeparator
|
||||||
|
? pathSeparatorBonus
|
||||||
|
: wordSeparatorBonus;
|
||||||
|
|
||||||
|
if (skipWordSeparators && seqLen == 0 && qi > 0)
|
||||||
|
{
|
||||||
|
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
|
||||||
|
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
|
||||||
|
|
||||||
|
if (qPrevKind == SymbolKind.WordSeparator)
|
||||||
|
{
|
||||||
|
score += separatorAlignmentBonus;
|
||||||
|
|
||||||
|
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
|
||||||
|
{
|
||||||
|
score += exactSeparatorBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camelCaseBonus != 0 && seqLen == 0 && char.IsUpper(Unsafe.Add(ref tRaw0, ti)))
|
||||||
|
{
|
||||||
|
score += camelCaseBonus;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rented is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<int>.Shared.Return(rented);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema ID is for cache invalidation of precomputed targets.
|
||||||
|
// Only includes options that affect folding/bloom, not scoring.
|
||||||
|
private static uint ComputeSchemaId(PrecomputedFuzzyMatcherOptions o)
|
||||||
|
{
|
||||||
|
const uint fnvOffset = 2166136261;
|
||||||
|
const uint fnvPrime = 16777619;
|
||||||
|
|
||||||
|
var h = fnvOffset;
|
||||||
|
h = unchecked((h ^ FolderSchemaVersion) * fnvPrime);
|
||||||
|
h = unchecked((h ^ BloomSchemaVersion) * fnvPrime);
|
||||||
|
h = unchecked((h ^ (uint)(o.RemoveDiacritics ? 1 : 0)) * fnvPrime);
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class PrecomputedFuzzyMatcherOptions
|
||||||
|
{
|
||||||
|
public static PrecomputedFuzzyMatcherOptions Default { get; } = new();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bonuses
|
||||||
|
*/
|
||||||
|
public int CharMatchBonus { get; init; } = 1;
|
||||||
|
|
||||||
|
public int SameCaseBonus { get; init; } = 1;
|
||||||
|
|
||||||
|
public int ConsecutiveMultiplier { get; init; } = 5;
|
||||||
|
|
||||||
|
public int CamelCaseBonus { get; init; } = 2;
|
||||||
|
|
||||||
|
public int StartOfWordBonus { get; init; } = 8;
|
||||||
|
|
||||||
|
public int PathSeparatorBonus { get; init; } = 5;
|
||||||
|
|
||||||
|
public int WordSeparatorBonus { get; init; } = 4;
|
||||||
|
|
||||||
|
public int SeparatorAlignmentBonus { get; init; } = 2;
|
||||||
|
|
||||||
|
public int ExactSeparatorBonus { get; init; } = 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Settings
|
||||||
|
*/
|
||||||
|
public bool RemoveDiacritics { get; init; } = true;
|
||||||
|
|
||||||
|
public bool SkipWordSeparators { get; init; } = true;
|
||||||
|
|
||||||
|
public bool IgnoreSameCaseBonusIfQueryIsAllLowercase { get; init; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using ToolGood.Words.Pinyin;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class PrecomputedFuzzyMatcherWithPinyin : IPrecomputedFuzzyMatcher
|
||||||
|
{
|
||||||
|
private readonly IBloomFilter _bloom;
|
||||||
|
private readonly PrecomputedFuzzyMatcher _core;
|
||||||
|
|
||||||
|
private readonly IStringFolder _stringFolder;
|
||||||
|
private readonly PinyinFuzzyMatcherOptions _pinyin;
|
||||||
|
|
||||||
|
public PrecomputedFuzzyMatcherWithPinyin(
|
||||||
|
PrecomputedFuzzyMatcherOptions coreOptions,
|
||||||
|
PinyinFuzzyMatcherOptions pinyinOptions,
|
||||||
|
IStringFolder stringFolder,
|
||||||
|
IBloomFilter bloom)
|
||||||
|
{
|
||||||
|
_pinyin = pinyinOptions;
|
||||||
|
_stringFolder = stringFolder;
|
||||||
|
_bloom = bloom;
|
||||||
|
|
||||||
|
_core = new PrecomputedFuzzyMatcher(coreOptions, stringFolder, bloom);
|
||||||
|
|
||||||
|
SchemaId = CombineSchema(_core.SchemaId, _pinyin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint SchemaId { get; }
|
||||||
|
|
||||||
|
public FuzzyQuery PrecomputeQuery(string? input)
|
||||||
|
{
|
||||||
|
input ??= string.Empty;
|
||||||
|
|
||||||
|
var primary = _core.PrecomputeQuery(input);
|
||||||
|
|
||||||
|
// Fast exit if effectively off (provider should already filter, but keep robust)
|
||||||
|
if (!IsPinyinEnabled(_pinyin))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match legacy: remove apostrophes for query secondary
|
||||||
|
var queryForPinyin = _pinyin.RemoveApostrophesForQuery ? RemoveApostrophesIfAny(input) : input;
|
||||||
|
|
||||||
|
var pinyin = WordsHelper.GetPinyin(queryForPinyin);
|
||||||
|
if (string.IsNullOrEmpty(pinyin))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondary = _core.PrecomputeQuery(pinyin);
|
||||||
|
return new FuzzyQuery(
|
||||||
|
primary.Original,
|
||||||
|
primary.Folded,
|
||||||
|
primary.Bloom,
|
||||||
|
primary.EffectiveLength,
|
||||||
|
primary.IsAllLowercaseAsciiOrNonLetter,
|
||||||
|
secondary.Original,
|
||||||
|
secondary.Folded,
|
||||||
|
secondary.Bloom,
|
||||||
|
secondary.EffectiveLength,
|
||||||
|
secondary.SecondaryIsAllLowercaseAsciiOrNonLetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FuzzyTarget PrecomputeTarget(string? input)
|
||||||
|
{
|
||||||
|
input ??= string.Empty;
|
||||||
|
|
||||||
|
var primary = _core.PrecomputeTarget(input);
|
||||||
|
|
||||||
|
if (!IsPinyinEnabled(_pinyin))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match legacy: only compute target pinyin when target contains Chinese
|
||||||
|
if (!ContainsToolGoodChinese(input))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinyin = WordsHelper.GetPinyin(input);
|
||||||
|
if (string.IsNullOrEmpty(pinyin))
|
||||||
|
{
|
||||||
|
return primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondary = _core.PrecomputeTarget(pinyin);
|
||||||
|
return new FuzzyTarget(
|
||||||
|
primary.Original,
|
||||||
|
primary.Folded,
|
||||||
|
primary.Bloom,
|
||||||
|
secondary.Original,
|
||||||
|
secondary.Folded,
|
||||||
|
secondary.Bloom);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target)
|
||||||
|
=> _core.Score(in query, in target);
|
||||||
|
|
||||||
|
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o) => o.Mode switch
|
||||||
|
{
|
||||||
|
PinyinMode.Off => false,
|
||||||
|
PinyinMode.On => true,
|
||||||
|
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool IsSimplifiedChineseUi()
|
||||||
|
{
|
||||||
|
var culture = CultureInfo.CurrentUICulture;
|
||||||
|
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsToolGoodChinese(string s)
|
||||||
|
{
|
||||||
|
return WordsHelper.HasChinese(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveApostrophesIfAny(string input)
|
||||||
|
{
|
||||||
|
var first = input.IndexOf('\'');
|
||||||
|
if (first < 0)
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeCount = 1;
|
||||||
|
for (var i = first + 1; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
if (input[i] == '\'')
|
||||||
|
{
|
||||||
|
removeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Create(input.Length - removeCount, input, static (dst, src) =>
|
||||||
|
{
|
||||||
|
var di = 0;
|
||||||
|
for (var i = 0; i < src.Length; i++)
|
||||||
|
{
|
||||||
|
var c = src[i];
|
||||||
|
if (c == '\'')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst[di++] = c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint CombineSchema(uint coreSchemaId, PinyinFuzzyMatcherOptions p)
|
||||||
|
{
|
||||||
|
const uint fnvOffset = 2166136261;
|
||||||
|
const uint fnvPrime = 16777619;
|
||||||
|
|
||||||
|
var h = fnvOffset;
|
||||||
|
h = unchecked((h ^ coreSchemaId) * fnvPrime);
|
||||||
|
h = unchecked((h ^ (uint)p.Mode) * fnvPrime);
|
||||||
|
h = unchecked((h ^ (p.RemoveApostrophesForQuery ? 1u : 0u)) * fnvPrime);
|
||||||
|
|
||||||
|
// bump if you change formatting/conversion behavior
|
||||||
|
const uint pinyinAlgoVersion = 1;
|
||||||
|
h = unchecked((h ^ pinyinAlgoVersion) * fnvPrime);
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
public sealed class StringFolder : IStringFolder
|
||||||
|
{
|
||||||
|
// Cache for diacritic-stripped uppercase characters.
|
||||||
|
// Benign race: worst case is redundant computation writing the same value.
|
||||||
|
// 0 = uncached, else cachedChar + 1
|
||||||
|
private static readonly ushort[] StripCacheUpper = new ushort[char.MaxValue + 1];
|
||||||
|
|
||||||
|
public string Fold(string input, bool removeDiacritics)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removeDiacritics || Ascii.IsValid(input))
|
||||||
|
{
|
||||||
|
if (IsAlreadyFoldedAndSlashNormalized(input))
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Create(input.Length, input, static (dst, src) =>
|
||||||
|
{
|
||||||
|
for (var i = 0; i < src.Length; i++)
|
||||||
|
{
|
||||||
|
var c = src[i];
|
||||||
|
dst[i] = c == '\\' ? '/' : char.ToUpperInvariant(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Create(input.Length, input, static (dst, src) =>
|
||||||
|
{
|
||||||
|
for (var i = 0; i < src.Length; i++)
|
||||||
|
{
|
||||||
|
var c = src[i];
|
||||||
|
var upper = c == '\\' ? '/' : char.ToUpperInvariant(c);
|
||||||
|
dst[i] = StripDiacriticsFromUpper(upper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAlreadyFoldedAndSlashNormalized(string input)
|
||||||
|
{
|
||||||
|
var sawNonAscii = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
var c = input[i];
|
||||||
|
|
||||||
|
if (c == '\\')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((uint)(c - 'a') <= 'z' - 'a')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c > 0x7F)
|
||||||
|
{
|
||||||
|
sawNonAscii = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sawNonAscii)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
var c = input[i];
|
||||||
|
if (c <= 0x7F)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||||
|
if (cat is UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static char StripDiacriticsFromUpper(char upper)
|
||||||
|
{
|
||||||
|
if (upper <= 0x7F)
|
||||||
|
{
|
||||||
|
return upper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never attempt normalization on lone UTF-16 surrogates.
|
||||||
|
if (char.IsSurrogate(upper))
|
||||||
|
{
|
||||||
|
return upper;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedPlus1 = StripCacheUpper[upper];
|
||||||
|
if (cachedPlus1 != 0)
|
||||||
|
{
|
||||||
|
return (char)(cachedPlus1 - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapped = StripDiacriticsSlow(upper);
|
||||||
|
StripCacheUpper[upper] = (ushort)(mapped + 1);
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static char StripDiacriticsSlow(char upper)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseChar = FirstNonMark(upper, NormalizationForm.FormD);
|
||||||
|
if (baseChar == '\0' || baseChar == upper)
|
||||||
|
{
|
||||||
|
var kd = FirstNonMark(upper, NormalizationForm.FormKD);
|
||||||
|
if (kd != '\0')
|
||||||
|
{
|
||||||
|
baseChar = kd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return char.ToUpperInvariant(baseChar == '\0' ? upper : baseChar);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Absolute safety: if globalization tables ever throw for some reason,
|
||||||
|
// degrade gracefully rather than failing hard.
|
||||||
|
return upper;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char FirstNonMark(char c, NormalizationForm form)
|
||||||
|
{
|
||||||
|
var normalized = c.ToString().Normalize(form);
|
||||||
|
|
||||||
|
foreach (var ch in normalized)
|
||||||
|
{
|
||||||
|
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (cat is not (UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark or UnicodeCategory.EnclosingMark))
|
||||||
|
{
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
internal static class SymbolClassifier
|
||||||
|
{
|
||||||
|
// Embedded in .data section - no allocation, no static constructor
|
||||||
|
private static ReadOnlySpan<byte> Lookup =>
|
||||||
|
[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
|
||||||
|
2, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 1, // 32-47: space=2, "=2, '=2, -=2, .=2, /=1
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, // 48-63: :=2
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64-79
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, // 80-95: _=2
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96-111
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 112-127
|
||||||
|
];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static SymbolKind Classify(char c)
|
||||||
|
{
|
||||||
|
return c > 0x7F ? SymbolKind.Other : (SymbolKind)Lookup[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.Core.Common.Text;
|
||||||
|
|
||||||
|
internal enum SymbolKind : byte
|
||||||
|
{
|
||||||
|
Other = 0,
|
||||||
|
PathSeparator = 1,
|
||||||
|
WordSeparator = 2,
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.CmdPal.Core.Common;
|
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.Messages;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@@ -13,7 +15,7 @@ using Windows.ApplicationModel.DataTransfer;
|
|||||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||||
|
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||||
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext
|
public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext, IPrecomputedListItem
|
||||||
{
|
{
|
||||||
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
public ExtensionObject<ICommandItem> Model => _commandItemModel;
|
||||||
|
|
||||||
@@ -22,6 +24,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
|
||||||
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
private CommandContextItemViewModel? _defaultCommandContextItemViewModel;
|
||||||
|
|
||||||
|
private FuzzyTargetCache _titleCache;
|
||||||
|
private FuzzyTargetCache _subtitleCache;
|
||||||
|
|
||||||
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
|
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
|
||||||
|
|
||||||
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
|
protected bool IsFastInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.FastInitialized);
|
||||||
@@ -116,6 +121,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
|
|
||||||
_itemTitle = model.Title;
|
_itemTitle = model.Title;
|
||||||
Subtitle = model.Subtitle;
|
Subtitle = model.Subtitle;
|
||||||
|
_titleCache.Invalidate();
|
||||||
|
_subtitleCache.Invalidate();
|
||||||
|
|
||||||
Initialized |= InitializedState.FastInitialized;
|
Initialized |= InitializedState.FastInitialized;
|
||||||
}
|
}
|
||||||
@@ -249,6 +256,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
Subtitle = "Item failed to load";
|
Subtitle = "Item failed to load";
|
||||||
MoreCommands = [];
|
MoreCommands = [];
|
||||||
_icon = _errorIcon;
|
_icon = _errorIcon;
|
||||||
|
_titleCache.Invalidate();
|
||||||
|
_subtitleCache.Invalidate();
|
||||||
Initialized |= InitializedState.Error;
|
Initialized |= InitializedState.Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +295,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
Subtitle = "Item failed to load";
|
Subtitle = "Item failed to load";
|
||||||
MoreCommands = [];
|
MoreCommands = [];
|
||||||
_icon = _errorIcon;
|
_icon = _errorIcon;
|
||||||
|
_titleCache.Invalidate();
|
||||||
|
_subtitleCache.Invalidate();
|
||||||
Initialized |= InitializedState.Error;
|
Initialized |= InitializedState.Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,12 +346,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
|
|
||||||
case nameof(Title):
|
case nameof(Title):
|
||||||
_itemTitle = model.Title;
|
_itemTitle = model.Title;
|
||||||
|
_titleCache.Invalidate();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nameof(Subtitle):
|
case nameof(Subtitle):
|
||||||
var modelSubtitle = model.Subtitle;
|
var modelSubtitle = model.Subtitle;
|
||||||
this.Subtitle = modelSubtitle;
|
this.Subtitle = modelSubtitle;
|
||||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||||
|
_subtitleCache.Invalidate();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case nameof(Icon):
|
case nameof(Icon):
|
||||||
@@ -415,6 +428,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
// Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command
|
// 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.
|
// 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;
|
_itemTitle = model.Title;
|
||||||
|
_titleCache.Invalidate();
|
||||||
UpdateProperty(nameof(Title), nameof(Name));
|
UpdateProperty(nameof(Title), nameof(Name));
|
||||||
|
|
||||||
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
|
_defaultCommandContextItemViewModel?.UpdateTitle(model.Command.Name);
|
||||||
@@ -436,6 +450,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
private void UpdateTitle(string? title)
|
private void UpdateTitle(string? title)
|
||||||
{
|
{
|
||||||
_itemTitle = title ?? string.Empty;
|
_itemTitle = title ?? string.Empty;
|
||||||
|
_titleCache.Invalidate();
|
||||||
UpdateProperty(nameof(Title));
|
UpdateProperty(nameof(Title));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +471,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
|||||||
UpdateProperty(nameof(DataPackage));
|
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()
|
protected override void UnsafeCleanup()
|
||||||
{
|
{
|
||||||
base.UnsafeCleanup();
|
base.UnsafeCleanup();
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.CmdPal.Core.Common;
|
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.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -16,6 +19,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
|||||||
public partial class ContextMenuViewModel : ObservableObject,
|
public partial class ContextMenuViewModel : ObservableObject,
|
||||||
IRecipient<UpdateCommandBarMessage>
|
IRecipient<UpdateCommandBarMessage>
|
||||||
{
|
{
|
||||||
|
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||||
|
|
||||||
public ICommandBarContext? SelectedItem
|
public ICommandBarContext? SelectedItem
|
||||||
{
|
{
|
||||||
get => field;
|
get => field;
|
||||||
@@ -39,8 +44,9 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
|
|
||||||
private string _lastSearchText = string.Empty;
|
private string _lastSearchText = string.Empty;
|
||||||
|
|
||||||
public ContextMenuViewModel()
|
public ContextMenuViewModel(IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||||
{
|
{
|
||||||
|
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
||||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +97,14 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
.OfType<CommandContextItemViewModel>()
|
.OfType<CommandContextItemViewModel>()
|
||||||
.Where(c => c.ShouldBeVisible);
|
.Where(c => c.ShouldBeVisible);
|
||||||
|
|
||||||
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
|
var query = _fuzzyMatcherProvider.Current.PrecomputeQuery(searchText);
|
||||||
|
var newResults = InternalListHelpers.FilterList(commands, in query, ScoreFunction);
|
||||||
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
|
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
|
private int ScoreFunction(in FuzzyQuery query, CommandContextItemViewModel item)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
|
if (string.IsNullOrWhiteSpace(query.Original))
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -107,11 +114,21 @@ public partial class ContextMenuViewModel : ObservableObject,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Title);
|
var fuzzyMatcher = _fuzzyMatcherProvider.Current;
|
||||||
|
var title = item.GetTitleTarget(fuzzyMatcher);
|
||||||
|
var subtitle = item.GetSubtitleTarget(fuzzyMatcher);
|
||||||
|
|
||||||
var descriptionMatch = FuzzyStringMatcher.ScoreFuzzy(query, item.Subtitle);
|
var titleScore = fuzzyMatcher.Score(query, title);
|
||||||
|
var subtitleScore = (fuzzyMatcher.Score(query, subtitle) - 4) / 2;
|
||||||
|
|
||||||
return new[] { nameMatch, (descriptionMatch - 4) / 2, 0 }.Max();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
|||||||
|
|
||||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||||
{
|
{
|
||||||
|
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||||
|
|
||||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||||
|
|
||||||
// row 0
|
// row 0
|
||||||
@@ -128,10 +130,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
OnPropertyChanged(nameof(IsColorIntensityVisible));
|
||||||
|
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
|
||||||
|
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||||
|
OnPropertyChanged(nameof(IsResetButtonVisible));
|
||||||
|
|
||||||
if (value == ColorizationMode.WindowsAccentColor)
|
if (value == ColorizationMode.WindowsAccentColor)
|
||||||
{
|
{
|
||||||
@@ -179,6 +184,19 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
{
|
{
|
||||||
_settings.CustomThemeColorIntensity = value;
|
_settings.CustomThemeColorIntensity = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BackgroundImageTintIntensity
|
||||||
|
{
|
||||||
|
get => _settings.BackgroundImageTintIntensity;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_settings.BackgroundImageTintIntensity = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(EffectiveTintIntensity));
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,12 +297,108 @@ 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]
|
[ObservableProperty]
|
||||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||||
|
|
||||||
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||||
|
|
||||||
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
|
||||||
|
|
||||||
|
public bool 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 IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||||
|
|
||||||
@@ -292,16 +406,21 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
|
|
||||||
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||||
|
|
||||||
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
|
||||||
|
|
||||||
|
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
|
||||||
|
|
||||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||||
|
|
||||||
public Color EffectiveThemeColor => ColorizationMode switch
|
public Color EffectiveThemeColor =>
|
||||||
{
|
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
|
||||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
? Colors.Transparent
|
||||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
: ColorizationMode switch
|
||||||
_ => Colors.Transparent,
|
{
|
||||||
};
|
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).
|
// 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);
|
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||||
@@ -309,11 +428,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||||
|
|
||||||
public ImageSource? EffectiveBackgroundImageSource =>
|
public ImageSource? EffectiveBackgroundImageSource =>
|
||||||
ColorizationMode is ColorizationMode.Image
|
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
|
||||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
? null
|
||||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
: ColorizationMode is ColorizationMode.Image
|
||||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||||
: null;
|
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||||
|
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||||
|
: null;
|
||||||
|
|
||||||
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||||
{
|
{
|
||||||
@@ -327,7 +448,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
|
|
||||||
Reapply();
|
Reapply();
|
||||||
|
|
||||||
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
|
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||||
@@ -357,6 +478,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
// Theme services recalculates effective color and opacity based on current settings.
|
// Theme services recalculates effective color and opacity based on current settings.
|
||||||
EffectiveBackdrop = _themeService.Current.BackdropParameters;
|
EffectiveBackdrop = _themeService.Current.BackdropParameters;
|
||||||
OnPropertyChanged(nameof(EffectiveBackdrop));
|
OnPropertyChanged(nameof(EffectiveBackdrop));
|
||||||
|
OnPropertyChanged(nameof(EffectiveBackdropStyle));
|
||||||
|
OnPropertyChanged(nameof(EffectiveImageOpacity));
|
||||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||||
@@ -379,7 +502,28 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
|||||||
BackgroundImageBlurAmount = 0;
|
BackgroundImageBlurAmount = 0;
|
||||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||||
BackgroundImageOpacity = 100;
|
BackgroundImageOpacity = 100;
|
||||||
ColorIntensity = 0;
|
BackgroundImageTintIntensity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ResetAppearanceSettings()
|
||||||
|
{
|
||||||
|
// Reset theme
|
||||||
|
Theme = UserTheme.Default;
|
||||||
|
|
||||||
|
// Reset backdrop settings
|
||||||
|
BackdropStyleIndex = (int)BackdropStyle.Acrylic;
|
||||||
|
BackdropOpacity = 100;
|
||||||
|
|
||||||
|
// Reset background image settings
|
||||||
|
BackgroundImagePath = string.Empty;
|
||||||
|
ResetBackgroundImageProperties();
|
||||||
|
|
||||||
|
// Reset colorization
|
||||||
|
ColorizationMode = ColorizationMode.None;
|
||||||
|
ThemeColor = DefaultTintColor;
|
||||||
|
ColorIntensity = 100;
|
||||||
|
BackgroundImageTintIntensity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the type of system backdrop controller to use.
|
||||||
|
/// </summary>
|
||||||
|
public enum BackdropControllerKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Solid color with alpha transparency (TransparentTintBackdrop).
|
||||||
|
/// </summary>
|
||||||
|
Solid,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desktop Acrylic with default blur (DesktopAcrylicKind.Default).
|
||||||
|
/// </summary>
|
||||||
|
Acrylic,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desktop Acrylic with thinner blur (DesktopAcrylicKind.Thin).
|
||||||
|
/// </summary>
|
||||||
|
AcrylicThin,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mica effect (MicaKind.Base).
|
||||||
|
/// </summary>
|
||||||
|
Mica,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mica alternate/darker variant (MicaKind.BaseAlt).
|
||||||
|
/// </summary>
|
||||||
|
MicaAlt,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom backdrop implementation.
|
||||||
|
/// </summary>
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the visual backdrop style for the window.
|
||||||
|
/// </summary>
|
||||||
|
public enum BackdropStyle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Standard desktop acrylic with blur effect.
|
||||||
|
/// </summary>
|
||||||
|
Acrylic,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solid color with alpha transparency (no blur).
|
||||||
|
/// </summary>
|
||||||
|
Clear,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mica effect that samples the desktop wallpaper.
|
||||||
|
/// </summary>
|
||||||
|
Mica,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thinner acrylic variant with more transparency.
|
||||||
|
/// </summary>
|
||||||
|
AcrylicThin,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mica alternate variant (darker).
|
||||||
|
/// </summary>
|
||||||
|
MicaAlt,
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration parameters for a backdrop style.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record BackdropStyleConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of system backdrop controller to use.
|
||||||
|
/// </summary>
|
||||||
|
public required BackdropControllerKind ControllerKind { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the base tint opacity before user adjustments.
|
||||||
|
/// </summary>
|
||||||
|
public required float BaseTintOpacity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the base luminosity opacity before user adjustments.
|
||||||
|
/// </summary>
|
||||||
|
public required float BaseLuminosityOpacity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the brush type to use for preview approximation.
|
||||||
|
/// </summary>
|
||||||
|
public required PreviewBrushKind PreviewBrush { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the fixed opacity for styles that don't support user adjustment (e.g., Mica).
|
||||||
|
/// When <see cref="SupportsOpacity"/> is false, this value is used as the effective opacity.
|
||||||
|
/// </summary>
|
||||||
|
public float FixedOpacity { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this backdrop style supports custom colorization (tint colors).
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsColorization { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this backdrop style supports custom background images.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsBackgroundImage { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether this backdrop style supports opacity adjustment.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsOpacity { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the effective tint opacity based on this style's configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userOpacity">User's backdrop opacity setting (0-1 normalized).</param>
|
||||||
|
/// <param name="baseTintOpacityOverride">Optional override for base tint opacity (used by colorful theme).</param>
|
||||||
|
/// <returns>The effective opacity to apply.</returns>
|
||||||
|
public float ComputeEffectiveOpacity(float userOpacity, float? baseTintOpacityOverride = null)
|
||||||
|
{
|
||||||
|
// For styles that don't support opacity (Mica), use FixedOpacity
|
||||||
|
if (!SupportsOpacity && FixedOpacity > 0)
|
||||||
|
{
|
||||||
|
return FixedOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Solid: only user opacity matters (controls alpha of solid color)
|
||||||
|
if (ControllerKind == BackdropControllerKind.Solid)
|
||||||
|
{
|
||||||
|
return userOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For blur effects: multiply base opacity with user opacity
|
||||||
|
var baseTint = baseTintOpacityOverride ?? BaseTintOpacity;
|
||||||
|
return baseTint * userOpacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central registry of backdrop style configurations.
|
||||||
|
/// </summary>
|
||||||
|
public static class BackdropStyles
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<BackdropStyle, BackdropStyleConfig> Configs = new()
|
||||||
|
{
|
||||||
|
[BackdropStyle.Acrylic] = new()
|
||||||
|
{
|
||||||
|
ControllerKind = BackdropControllerKind.Acrylic,
|
||||||
|
BaseTintOpacity = 0.5f,
|
||||||
|
BaseLuminosityOpacity = 0.9f,
|
||||||
|
PreviewBrush = PreviewBrushKind.Acrylic,
|
||||||
|
},
|
||||||
|
[BackdropStyle.AcrylicThin] = new()
|
||||||
|
{
|
||||||
|
ControllerKind = BackdropControllerKind.AcrylicThin,
|
||||||
|
BaseTintOpacity = 0.0f,
|
||||||
|
BaseLuminosityOpacity = 0.85f,
|
||||||
|
PreviewBrush = PreviewBrushKind.Acrylic,
|
||||||
|
},
|
||||||
|
[BackdropStyle.Mica] = new()
|
||||||
|
{
|
||||||
|
ControllerKind = BackdropControllerKind.Mica,
|
||||||
|
BaseTintOpacity = 0.0f,
|
||||||
|
BaseLuminosityOpacity = 1.0f,
|
||||||
|
PreviewBrush = PreviewBrushKind.Solid,
|
||||||
|
FixedOpacity = 0.96f,
|
||||||
|
SupportsOpacity = false,
|
||||||
|
},
|
||||||
|
[BackdropStyle.MicaAlt] = new()
|
||||||
|
{
|
||||||
|
ControllerKind = BackdropControllerKind.MicaAlt,
|
||||||
|
BaseTintOpacity = 0.0f,
|
||||||
|
BaseLuminosityOpacity = 1.0f,
|
||||||
|
PreviewBrush = PreviewBrushKind.Solid,
|
||||||
|
FixedOpacity = 0.98f,
|
||||||
|
SupportsOpacity = false,
|
||||||
|
},
|
||||||
|
[BackdropStyle.Clear] = new()
|
||||||
|
{
|
||||||
|
ControllerKind = BackdropControllerKind.Solid,
|
||||||
|
BaseTintOpacity = 1.0f,
|
||||||
|
BaseLuminosityOpacity = 1.0f,
|
||||||
|
PreviewBrush = PreviewBrushKind.Solid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configuration for the specified backdrop style.
|
||||||
|
/// </summary>
|
||||||
|
public static BackdropStyleConfig Get(BackdropStyle style) =>
|
||||||
|
Configs.TryGetValue(style, out var config) ? config : Configs[BackdropStyle.Acrylic];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all registered backdrop styles.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<BackdropStyle> All => Configs.Keys;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.Ext.Apps;
|
using Microsoft.CmdPal.Ext.Apps;
|
||||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||||
@@ -24,7 +25,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"/>.
|
/// 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.
|
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainListPage : DynamicListPage,
|
public sealed partial class MainListPage : DynamicListPage,
|
||||||
IRecipient<ClearSearchMessage>,
|
IRecipient<ClearSearchMessage>,
|
||||||
IRecipient<UpdateFallbackItemsMessage>, IDisposable
|
IRecipient<UpdateFallbackItemsMessage>, IDisposable
|
||||||
{
|
{
|
||||||
@@ -32,13 +33,18 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
private readonly AliasManager _aliasManager;
|
private readonly AliasManager _aliasManager;
|
||||||
private readonly SettingsModel _settings;
|
private readonly SettingsModel _settings;
|
||||||
private readonly AppStateModel _appStateModel;
|
private readonly AppStateModel _appStateModel;
|
||||||
private List<Scored<IListItem>>? _filteredItems;
|
private readonly ScoringFunction<IListItem> _scoringFunction;
|
||||||
private List<Scored<IListItem>>? _filteredApps;
|
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
||||||
|
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||||
|
|
||||||
|
private RoScored<IListItem>[]? _filteredItems;
|
||||||
|
private RoScored<IListItem>[]? _filteredApps;
|
||||||
|
|
||||||
// Keep as IEnumerable for deferred execution. Fallback item titles are updated
|
// Keep as IEnumerable for deferred execution. Fallback item titles are updated
|
||||||
// asynchronously, so scoring must happen lazily when GetItems is called.
|
// asynchronously, so scoring must happen lazily when GetItems is called.
|
||||||
private IEnumerable<Scored<IListItem>>? _scoredFallbackItems;
|
private IEnumerable<RoScored<IListItem>>? _scoredFallbackItems;
|
||||||
private IEnumerable<Scored<IListItem>>? _fallbackItems;
|
private IEnumerable<RoScored<IListItem>>? _fallbackItems;
|
||||||
|
|
||||||
private bool _includeApps;
|
private bool _includeApps;
|
||||||
private bool _filteredItemsIncludesApps;
|
private bool _filteredItemsIncludesApps;
|
||||||
private int _appResultLimit = 10;
|
private int _appResultLimit = 10;
|
||||||
@@ -48,7 +54,12 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
|
|
||||||
private CancellationTokenSource? _cancellationTokenSource;
|
private CancellationTokenSource? _cancellationTokenSource;
|
||||||
|
|
||||||
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
public MainListPage(
|
||||||
|
TopLevelCommandManager topLevelCommandManager,
|
||||||
|
SettingsModel settings,
|
||||||
|
AliasManager aliasManager,
|
||||||
|
AppStateModel appStateModel,
|
||||||
|
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||||
{
|
{
|
||||||
Title = Resources.builtin_home_name;
|
Title = Resources.builtin_home_name;
|
||||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||||
@@ -58,6 +69,10 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
_aliasManager = aliasManager;
|
_aliasManager = aliasManager;
|
||||||
_appStateModel = appStateModel;
|
_appStateModel = appStateModel;
|
||||||
_tlcManager = topLevelCommandManager;
|
_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.PropertyChanged += TlcManager_PropertyChanged;
|
||||||
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
||||||
|
|
||||||
@@ -190,8 +205,7 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
|
|
||||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||||
{
|
{
|
||||||
var timer = new Stopwatch();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
timer.Start();
|
|
||||||
|
|
||||||
_cancellationTokenSource?.Cancel();
|
_cancellationTokenSource?.Cancel();
|
||||||
_cancellationTokenSource?.Dispose();
|
_cancellationTokenSource?.Dispose();
|
||||||
@@ -354,15 +368,14 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
|
|
||||||
if (_includeApps)
|
if (_includeApps)
|
||||||
{
|
{
|
||||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().ToList();
|
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();
|
||||||
|
|
||||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||||
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
||||||
|
|
||||||
if (pinnedApps.Length > 0)
|
if (pinnedApps.Length > 0)
|
||||||
{
|
{
|
||||||
newApps = allNewApps.Where(w =>
|
newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0);
|
||||||
pinnedApps.IndexOf(((AppListItem)w).AppIdentifier) < 0);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -376,11 +389,10 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var history = _appStateModel.RecentCommands!;
|
var searchQuery = _fuzzyMatcherProvider.Current.PrecomputeQuery(SearchText);
|
||||||
Func<string, IListItem, int> scoreItem = (a, b) => { return ScoreTopLevelItem(a, b, history); };
|
|
||||||
|
|
||||||
// Produce a list of everything that matches the current filter.
|
// Produce a list of everything that matches the current filter.
|
||||||
_filteredItems = [.. ListHelpers.FilterListWithScores<IListItem>(newFilteredItems ?? [], SearchText, scoreItem)];
|
_filteredItems = InternalListHelpers.FilterListWithScores(newFilteredItems, searchQuery, _scoringFunction);
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -388,21 +400,14 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
|
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
|
||||||
|
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_scoredFallbackItems = ListHelpers.FilterListWithScores<IListItem>(newFallbacksForScoring ?? [], SearchText, scoreItem);
|
_fallbackItems = InternalListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], searchQuery, _fallbackScoringFunction);
|
||||||
|
|
||||||
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)
|
if (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -412,18 +417,7 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
// Produce a list of filtered apps with the appropriate limit
|
// Produce a list of filtered apps with the appropriate limit
|
||||||
if (newApps.Any())
|
if (newApps.Any())
|
||||||
{
|
{
|
||||||
var scoredApps = ListHelpers.FilterListWithScores<IListItem>(newApps, SearchText, scoreItem);
|
_filteredApps = InternalListHelpers.FilterListWithScores(newApps, searchQuery, _scoringFunction);
|
||||||
|
|
||||||
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)
|
if (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -431,10 +425,15 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
|
||||||
|
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
|
||||||
|
|
||||||
RaiseItemsChanged();
|
RaiseItemsChanged();
|
||||||
|
|
||||||
timer.Stop();
|
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
|
||||||
Logger.LogDebug($"Filter with '{newSearch}' in {timer.ElapsedMilliseconds}ms");
|
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +477,11 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
||||||
// fact that we want fallback handlers down-weighted, so that they don't
|
// fact that we want fallback handlers down-weighted, so that they don't
|
||||||
// _always_ show up first.
|
// _always_ show up first.
|
||||||
internal static int ScoreTopLevelItem(string query, IListItem topLevelOrAppItem, IRecentCommandsManager history)
|
internal static int ScoreTopLevelItem(
|
||||||
|
in FuzzyQuery query,
|
||||||
|
IListItem topLevelOrAppItem,
|
||||||
|
IRecentCommandsManager history,
|
||||||
|
IPrecomputedFuzzyMatcher precomputedFuzzyMatcher)
|
||||||
{
|
{
|
||||||
var title = topLevelOrAppItem.Title;
|
var title = topLevelOrAppItem.Title;
|
||||||
if (string.IsNullOrWhiteSpace(title))
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
@@ -486,94 +489,80 @@ public partial class MainListPage : DynamicListPage,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isWhiteSpace = string.IsNullOrWhiteSpace(query);
|
|
||||||
|
|
||||||
var isFallback = false;
|
var isFallback = false;
|
||||||
var isAliasSubstringMatch = false;
|
var isAliasSubstringMatch = false;
|
||||||
var isAliasMatch = false;
|
var isAliasMatch = false;
|
||||||
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
||||||
|
|
||||||
var extensionDisplayName = string.Empty;
|
FuzzyTarget? extensionDisplayNameTarget = null;
|
||||||
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
||||||
{
|
{
|
||||||
isFallback = topLevel.IsFallback;
|
isFallback = topLevel.IsFallback;
|
||||||
|
extensionDisplayNameTarget = topLevel.GetExtensionNameTarget(precomputedFuzzyMatcher);
|
||||||
|
|
||||||
if (topLevel.HasAlias)
|
if (topLevel.HasAlias)
|
||||||
{
|
{
|
||||||
var alias = topLevel.AliasText;
|
var alias = topLevel.AliasText;
|
||||||
isAliasMatch = alias == query;
|
isAliasMatch = alias == query.Original;
|
||||||
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query, StringComparison.CurrentCultureIgnoreCase);
|
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query.Original, StringComparison.CurrentCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionDisplayName = topLevel.ExtensionHost?.Extension?.PackageDisplayName ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringMatcher.FuzzySearch will absolutely BEEF IT if you give it a
|
// Handle whitespace query separately - FuzzySearch doesn't handle it well
|
||||||
// whitespace-only query.
|
if (string.IsNullOrWhiteSpace(query.Original))
|
||||||
//
|
|
||||||
// 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[]
|
|
||||||
{
|
{
|
||||||
nameMatch,
|
return ScoreWhitespaceQuery(query.Original, title, topLevelOrAppItem.Subtitle, isFallback);
|
||||||
descriptionMatch,
|
}
|
||||||
isFallback ? 1 : 0, // Always give fallbacks a chance
|
|
||||||
};
|
|
||||||
var max = scores.Max();
|
|
||||||
|
|
||||||
// _Add_ the extension name. This will bubble items that match both
|
// Get precomputed targets
|
||||||
// title and extension name up above ones that just match title.
|
var (titleTarget, subtitleTarget) = topLevelOrAppItem is IPrecomputedListItem precomputedItem
|
||||||
// e.g. "git" will up-weight "GitHub searches" from the GitHub extension
|
? (precomputedItem.GetTitleTarget(precomputedFuzzyMatcher), precomputedItem.GetSubtitleTarget(precomputedFuzzyMatcher))
|
||||||
// above "git" from "whatever"
|
: (precomputedFuzzyMatcher.PrecomputeTarget(title), precomputedFuzzyMatcher.PrecomputeTarget(topLevelOrAppItem.Subtitle));
|
||||||
max = max + extensionTitleMatch;
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
// Apply a penalty to fallback items so they rank below direct matches.
|
// Apply a penalty to fallback items so they rank below direct matches.
|
||||||
// Fallbacks that dynamically match queries (like RDP connections) should
|
// Fallbacks that dynamically match queries (like RDP connections) should
|
||||||
// appear after apps and direct command matches.
|
// appear after apps and direct command matches.
|
||||||
if (isFallback && max > 1)
|
if (isFallback && matchScore > 1)
|
||||||
{
|
{
|
||||||
// Reduce fallback scores by 50% to prioritize direct matches
|
// Reduce fallback scores by 50% to prioritize direct matches
|
||||||
max = max * 0.5;
|
matchScore = matchScore * 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchSomething = max
|
// Alias matching: exact match is overwhelming priority, substring match adds a small boost
|
||||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
var aliasBoost = isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0);
|
||||||
|
var totalMatch = matchScore + aliasBoost;
|
||||||
|
|
||||||
// If we matched title, subtitle, or alias (something real), then
|
// Apply scaling and history boost only if we matched something real
|
||||||
// here we add the recent command weight boost
|
var finalScore = totalMatch * 10;
|
||||||
//
|
if (totalMatch > 0)
|
||||||
// Otherwise something like `x` will still match everything you've run before
|
|
||||||
var finalScore = matchSomething * 10;
|
|
||||||
if (matchSomething > 0)
|
|
||||||
{
|
{
|
||||||
var recentWeightBoost = history.GetCommandHistoryWeight(id);
|
finalScore += history.GetCommandHistoryWeight(id);
|
||||||
finalScore += recentWeightBoost;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)finalScore;
|
return (int)finalScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static int ScoreFallbackItem(string query, IListItem topLevelOrAppItem, string[] fallbackRanks)
|
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)
|
||||||
{
|
{
|
||||||
// Default to 1 so it always shows in list.
|
// Default to 1 so it always shows in list.
|
||||||
var finalScore = 1;
|
var finalScore = 1;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#pragma warning disable IDE0007 // Use implicit type
|
#pragma warning disable IDE0007 // Use implicit type
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
|
||||||
@@ -16,10 +17,10 @@ internal static class MainListPageResultFactory
|
|||||||
/// applying an application result limit and filtering fallback items as needed.
|
/// applying an application result limit and filtering fallback items as needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IListItem[] Create(
|
public static IListItem[] Create(
|
||||||
IList<Scored<IListItem>>? filteredItems,
|
IList<RoScored<IListItem>>? filteredItems,
|
||||||
IList<Scored<IListItem>>? scoredFallbackItems,
|
IList<RoScored<IListItem>>? scoredFallbackItems,
|
||||||
IList<Scored<IListItem>>? filteredApps,
|
IList<RoScored<IListItem>>? filteredApps,
|
||||||
IList<Scored<IListItem>>? fallbackItems,
|
IList<RoScored<IListItem>>? fallbackItems,
|
||||||
int appResultLimit)
|
int appResultLimit)
|
||||||
{
|
{
|
||||||
if (appResultLimit < 0)
|
if (appResultLimit < 0)
|
||||||
@@ -147,7 +148,7 @@ internal static class MainListPageResultFactory
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetNonEmptyFallbackItemsCount(IList<Scored<IListItem>>? fallbackItems)
|
private static int GetNonEmptyFallbackItemsCount(IList<RoScored<IListItem>>? fallbackItems)
|
||||||
{
|
{
|
||||||
int fallbackItemsCount = 0;
|
int fallbackItemsCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
|||||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
|
||||||
public partial double BackgroundImageOpacity { get; private set; }
|
public partial double BackgroundImageOpacity { get; private set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -39,6 +40,30 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool ShowBackgroundImage { get; private set; }
|
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)
|
public MainWindowViewModel(IThemeService themeService)
|
||||||
{
|
{
|
||||||
_themeService = themeService;
|
_themeService = themeService;
|
||||||
@@ -58,6 +83,9 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
|||||||
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
|
BackgroundImageTintIntensity = _themeService.Current.TintIntensity;
|
||||||
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
|
BackgroundImageBlurAmount = _themeService.Current.BlurAmount;
|
||||||
|
|
||||||
|
BackdropStyle = _themeService.Current.BackdropParameters.Style;
|
||||||
|
BackdropOpacity = _themeService.Current.BackdropOpacity;
|
||||||
|
|
||||||
ShowBackgroundImage = BackgroundImageSource != null;
|
ShowBackgroundImage = BackgroundImageSource != null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -1,9 +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;
|
|
||||||
|
|
||||||
public sealed record AcrylicBackdropParameters(Color TintColor, Color FallbackColor, float TintOpacity, float LuminosityOpacity);
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using 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);
|
||||||
@@ -51,12 +51,23 @@ public sealed class ThemeSnapshot
|
|||||||
public required double BackgroundImageOpacity { get; init; }
|
public required double BackgroundImageOpacity { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
/// Gets the effective backdrop parameters based on current settings and theme.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The resolved <c>AcrylicBackdropParameters</c> to apply.</returns>
|
/// <returns>The resolved <c>BackdropParameters</c> to apply.</returns>
|
||||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
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; }
|
||||||
|
|
||||||
public required int BlurAmount { get; init; }
|
public required int BlurAmount { get; init; }
|
||||||
|
|
||||||
public required float BackgroundBrightness { 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ public partial class SettingsModel : ObservableObject
|
|||||||
|
|
||||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||||
|
|
||||||
|
public int BackgroundImageTintIntensity { get; set; }
|
||||||
|
|
||||||
public int BackgroundImageOpacity { get; set; } = 20;
|
public int BackgroundImageOpacity { get; set; } = 20;
|
||||||
|
|
||||||
public int BackgroundImageBlurAmount { get; set; }
|
public int BackgroundImageBlurAmount { get; set; }
|
||||||
@@ -84,6 +86,10 @@ public partial class SettingsModel : ObservableObject
|
|||||||
|
|
||||||
public string? BackgroundImagePath { get; set; }
|
public string? BackgroundImagePath { get; set; }
|
||||||
|
|
||||||
|
public BackdropStyle BackdropStyle { get; set; }
|
||||||
|
|
||||||
|
public int BackdropOpacity { get; set; } = 100;
|
||||||
|
|
||||||
// END SETTINGS
|
// END SETTINGS
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||||
@@ -16,7 +19,8 @@ using WyHash;
|
|||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||||
|
|
||||||
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider
|
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
|
||||||
|
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider, IPrecomputedListItem
|
||||||
{
|
{
|
||||||
private readonly SettingsModel _settings;
|
private readonly SettingsModel _settings;
|
||||||
private readonly ProviderSettings _providerSettings;
|
private readonly ProviderSettings _providerSettings;
|
||||||
@@ -34,6 +38,10 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
private HotkeySettings? _hotkey;
|
private HotkeySettings? _hotkey;
|
||||||
private IIconInfo? _initialIcon;
|
private IIconInfo? _initialIcon;
|
||||||
|
|
||||||
|
private FuzzyTargetCache _titleCache;
|
||||||
|
private FuzzyTargetCache _subtitleCache;
|
||||||
|
private FuzzyTargetCache _extensionNameCache;
|
||||||
|
|
||||||
private CommandAlias? Alias { get; set; }
|
private CommandAlias? Alias { get; set; }
|
||||||
|
|
||||||
public bool IsFallback { get; private set; }
|
public bool IsFallback { get; private set; }
|
||||||
@@ -176,6 +184,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||||
|
|
||||||
public TopLevelViewModel(
|
public TopLevelViewModel(
|
||||||
CommandItemViewModel item,
|
CommandItemViewModel item,
|
||||||
bool isFallback,
|
bool isFallback,
|
||||||
@@ -230,6 +240,15 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
{
|
{
|
||||||
PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));
|
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))
|
if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
|
||||||
{
|
{
|
||||||
GenerateId();
|
GenerateId();
|
||||||
@@ -420,4 +439,18 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
|||||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
[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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using ManagedCommon;
|
|||||||
using Microsoft.CmdPal.Core.Common;
|
using Microsoft.CmdPal.Core.Common;
|
||||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
using Microsoft.CmdPal.Core.Common.Services;
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Ext.Apps;
|
using Microsoft.CmdPal.Ext.Apps;
|
||||||
using Microsoft.CmdPal.Ext.Bookmarks;
|
using Microsoft.CmdPal.Ext.Bookmarks;
|
||||||
@@ -67,11 +68,13 @@ public partial class App : Application, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
|
var appInfoService = new ApplicationInfoService();
|
||||||
|
|
||||||
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
|
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
|
||||||
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default);
|
_globalErrorHandler.Register(this, GlobalErrorHandler.Options.Default, appInfoService);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Services = ConfigureServices();
|
Services = ConfigureServices(appInfoService);
|
||||||
|
|
||||||
IconCacheProvider.Initialize(Services);
|
IconCacheProvider.Initialize(Services);
|
||||||
|
|
||||||
@@ -92,6 +95,9 @@ public partial class App : Application, IDisposable
|
|||||||
// This way, log statements from the core project will be captured by the PT logs
|
// This way, log statements from the core project will be captured by the PT logs
|
||||||
var logWrapper = new LogWrapper();
|
var logWrapper = new LogWrapper();
|
||||||
CoreLogger.InitializeLogger(logWrapper);
|
CoreLogger.InitializeLogger(logWrapper);
|
||||||
|
|
||||||
|
// Now that CoreLogger is initialized, initialize the logger delegate in ApplicationInfoService
|
||||||
|
appInfoService.SetLogDirectory(() => Logger.CurrentVersionLogDirectoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -109,7 +115,7 @@ public partial class App : Application, IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the services for the application
|
/// Configures the services for the application
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static ServiceProvider ConfigureServices()
|
private static ServiceProvider ConfigureServices(IApplicationInfoService appInfoService)
|
||||||
{
|
{
|
||||||
// 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
|
// 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();
|
ServiceCollection services = new();
|
||||||
@@ -120,7 +126,7 @@ public partial class App : Application, IDisposable
|
|||||||
|
|
||||||
AddBuiltInCommands(services);
|
AddBuiltInCommands(services);
|
||||||
|
|
||||||
AddCoreServices(services);
|
AddCoreServices(services, appInfoService);
|
||||||
|
|
||||||
AddUIServices(services, dispatcherQueue);
|
AddUIServices(services, dispatcherQueue);
|
||||||
|
|
||||||
@@ -196,9 +202,11 @@ public partial class App : Application, IDisposable
|
|||||||
services.AddIconServices(dispatcherQueue);
|
services.AddIconServices(dispatcherQueue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddCoreServices(ServiceCollection services)
|
private static void AddCoreServices(ServiceCollection services, IApplicationInfoService appInfoService)
|
||||||
{
|
{
|
||||||
// Core services
|
// Core services
|
||||||
|
services.AddSingleton(appInfoService);
|
||||||
|
|
||||||
services.AddSingleton<IExtensionService, ExtensionService>();
|
services.AddSingleton<IExtensionService, ExtensionService>();
|
||||||
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
services.AddSingleton<IRunHistoryService, RunHistoryService>();
|
||||||
|
|
||||||
@@ -206,6 +214,9 @@ public partial class App : Application, IDisposable
|
|||||||
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
|
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
|
||||||
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
|
services.AddSingleton<ITelemetryService, TelemetryForwarder>();
|
||||||
|
|
||||||
|
services.AddSingleton<IFuzzyMatcherProvider, FuzzyMatcherProvider>(
|
||||||
|
_ => new FuzzyMatcherProvider(new PrecomputedFuzzyMatcherOptions(), new PinyinFuzzyMatcherOptions()));
|
||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddSingleton<ShellViewModel>();
|
services.AddSingleton<ShellViewModel>();
|
||||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||||
|
|||||||
@@ -288,7 +288,6 @@ internal sealed partial class BlurImageControl : Control
|
|||||||
_effectBrush?.Dispose();
|
_effectBrush?.Dispose();
|
||||||
_effectBrush = effectFactory.CreateBrush();
|
_effectBrush = effectFactory.CreateBrush();
|
||||||
|
|
||||||
// Set initial source
|
|
||||||
if (ImageSource is not null)
|
if (ImageSource is not null)
|
||||||
{
|
{
|
||||||
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
_imageBrush ??= _compositor.CreateSurfaceBrush();
|
||||||
|
|||||||
@@ -16,24 +16,38 @@
|
|||||||
CornerRadius="8"
|
CornerRadius="8"
|
||||||
Translation="0,0,8">
|
Translation="0,0,8">
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<!-- Clear style: SolidColorBrush with computed alpha (window backdrop) -->
|
||||||
<Border
|
<Border
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
Visibility="{x:Bind h:BindTransformers.NegateVisibility(ShowBackgroundImage), Mode=OneWay}">
|
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}">
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<AcrylicBrush
|
<AcrylicBrush
|
||||||
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
FallbackColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||||
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
TintColor="{x:Bind PreviewBackgroundColor, Mode=OneWay}"
|
||||||
TintOpacity="{x:Bind PreviewBackgroundOpacity, Mode=OneWay}" />
|
TintOpacity="{x:Bind PreviewEffectiveOpacity, Mode=OneWay}" />
|
||||||
</Border.Background>
|
</Border.Background>
|
||||||
</Border>
|
</Border>
|
||||||
|
<!-- Background image (inside window, on top of backdrop) -->
|
||||||
<local:BlurImageControl
|
<local:BlurImageControl
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
|
BlurAmount="{x:Bind PreviewBackgroundImageBlurAmount, Mode=OneWay}"
|
||||||
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
|
ImageBrightness="{x:Bind PreviewBackgroundImageBrightness, Mode=OneWay}"
|
||||||
|
ImageOpacity="{x:Bind PreviewBackgroundImageOpacity, Mode=OneWay}"
|
||||||
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
|
ImageSource="{x:Bind PreviewBackgroundImageSource, Mode=OneWay}"
|
||||||
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
|
ImageStretch="{x:Bind ToStretch(PreviewBackgroundImageFit), Mode=OneWay}"
|
||||||
IsHitTestVisible="False"
|
IsHitTestVisible="False"
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ namespace Microsoft.CmdPal.UI.Controls;
|
|||||||
|
|
||||||
public sealed partial class CommandPalettePreview : UserControl
|
public sealed partial class CommandPalettePreview : UserControl
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty PreviewBackgroundOpacityProperty = DependencyProperty.Register(nameof(PreviewBackgroundOpacity), typeof(double), typeof(CommandPalettePreview), new PropertyMetadata(0d));
|
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color), OnBackdropPropertyChanged));
|
||||||
|
|
||||||
public static readonly DependencyProperty PreviewBackgroundColorProperty = DependencyProperty.Register(nameof(PreviewBackgroundColor), typeof(Color), typeof(CommandPalettePreview), new PropertyMetadata(default(Color)));
|
public static readonly DependencyProperty PreviewBackgroundImageSourceProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageSource), typeof(ImageSource), typeof(CommandPalettePreview), new PropertyMetadata(null, OnBackgroundImageSourceChanged));
|
||||||
|
|
||||||
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(double), typeof(CommandPalettePreview), new PropertyMetadata(1.0));
|
||||||
|
|
||||||
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)));
|
public static readonly DependencyProperty PreviewBackgroundImageFitProperty = DependencyProperty.Register(nameof(PreviewBackgroundImageFit), typeof(BackgroundImageFit), typeof(CommandPalettePreview), new PropertyMetadata(default(BackgroundImageFit)));
|
||||||
|
|
||||||
@@ -30,7 +28,18 @@ 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 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));
|
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 BackgroundImageFit PreviewBackgroundImageFit
|
public BackgroundImageFit PreviewBackgroundImageFit
|
||||||
{
|
{
|
||||||
@@ -38,12 +47,6 @@ public sealed partial class CommandPalettePreview : UserControl
|
|||||||
set { SetValue(PreviewBackgroundImageFitProperty, value); }
|
set { SetValue(PreviewBackgroundImageFitProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public double PreviewBackgroundOpacity
|
|
||||||
{
|
|
||||||
get { return (double)GetValue(PreviewBackgroundOpacityProperty); }
|
|
||||||
set { SetValue(PreviewBackgroundOpacityProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color PreviewBackgroundColor
|
public Color PreviewBackgroundColor
|
||||||
{
|
{
|
||||||
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
|
get { return (Color)GetValue(PreviewBackgroundColorProperty); }
|
||||||
@@ -56,10 +59,10 @@ public sealed partial class CommandPalettePreview : UserControl
|
|||||||
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
|
set { SetValue(PreviewBackgroundImageSourceProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public int PreviewBackgroundImageOpacity
|
public double PreviewBackgroundImageOpacity
|
||||||
{
|
{
|
||||||
get { return (int)GetValue(PreviewBackgroundImageOpacityProperty); }
|
get => (double)GetValue(PreviewBackgroundImageOpacityProperty);
|
||||||
set { SetValue(PreviewBackgroundImageOpacityProperty, value); }
|
set => SetValue(PreviewBackgroundImageOpacityProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public double PreviewBackgroundImageBrightness
|
public double PreviewBackgroundImageBrightness
|
||||||
@@ -92,12 +95,48 @@ public sealed partial class CommandPalettePreview : UserControl
|
|||||||
set => SetValue(ShowBackgroundImageProperty, value);
|
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()
|
public CommandPalettePreview()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnBackgroundImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (d is not CommandPalettePreview preview)
|
if (d is not CommandPalettePreview preview)
|
||||||
{
|
{
|
||||||
@@ -107,7 +146,46 @@ public sealed partial class CommandPalettePreview : UserControl
|
|||||||
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
|
preview.ShowBackgroundImage = e.NewValue is ImageSource ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double ToOpacity(int value) => value / 100.0;
|
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 ToTintIntensity(int value) => value / 100.0;
|
private double ToTintIntensity(int value) => value / 100.0;
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.Messages;
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Input;
|
using Microsoft.UI.Input;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
@@ -21,21 +23,19 @@ public sealed partial class ContextMenu : UserControl,
|
|||||||
IRecipient<UpdateCommandBarMessage>,
|
IRecipient<UpdateCommandBarMessage>,
|
||||||
IRecipient<TryCommandKeybindingMessage>
|
IRecipient<TryCommandKeybindingMessage>
|
||||||
{
|
{
|
||||||
public ContextMenuViewModel ViewModel { get; } = new();
|
public ContextMenuViewModel ViewModel { get; }
|
||||||
|
|
||||||
public ContextMenu()
|
public ContextMenu()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
ViewModel = new ContextMenuViewModel(App.Current.Services.GetRequiredService<IFuzzyMatcherProvider>());
|
||||||
|
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||||
|
|
||||||
// RegisterAll isn't AOT compatible
|
// RegisterAll isn't AOT compatible
|
||||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||||
|
|
||||||
if (ViewModel is not null)
|
|
||||||
{
|
|
||||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(OpenContextMenuMessage message)
|
public void Receive(OpenContextMenuMessage message)
|
||||||
|
|||||||
@@ -206,9 +206,10 @@
|
|||||||
|
|
||||||
<!-- More section -->
|
<!-- More section -->
|
||||||
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="More" />
|
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="More" />
|
||||||
<Border>
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<Button Command="{x:Bind ViewModel.OpenInternalToolsCommand}" Content="Open internal tools" />
|
<Button Command="{x:Bind ViewModel.OpenInternalToolsCommand}" Content="Open internal tools" />
|
||||||
</Border>
|
<Button Command="{x:Bind ViewModel.ToggleDevRibbonVisibilityCommand}" Content="Hide me" />
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ namespace Microsoft.CmdPal.UI.Controls;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class IconBox : ContentControl
|
public partial class IconBox : ContentControl
|
||||||
{
|
{
|
||||||
|
private const double DefaultIconFontSize = 16.0;
|
||||||
|
|
||||||
private double _lastScale;
|
private double _lastScale;
|
||||||
private ElementTheme _lastTheme;
|
private ElementTheme _lastTheme;
|
||||||
private double _lastFontSize;
|
private double _lastFontSize;
|
||||||
|
|
||||||
private const double DefaultIconFontSize = 16.0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
|
/// Gets or sets the <see cref="IconSource"/> to display within the <see cref="IconBox"/>. Overwritten, if <see cref="SourceKey"/> is used instead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -62,6 +62,12 @@ public partial class IconBox : ContentControl
|
|||||||
{
|
{
|
||||||
Refresh();
|
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;
|
remove => _sourceRequested -= value;
|
||||||
}
|
}
|
||||||
@@ -102,9 +108,12 @@ public partial class IconBox : ContentControl
|
|||||||
if (Source is FontIconSource fontIcon)
|
if (Source is FontIconSource fontIcon)
|
||||||
{
|
{
|
||||||
fontIcon.FontSize = _lastFontSize;
|
fontIcon.FontSize = _lastFontSize;
|
||||||
|
UpdatePaddingForFontIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdatePaddingForFontIcon() => Padding = new Thickness(Math.Round(_lastFontSize * -0.2));
|
||||||
|
|
||||||
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
private void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||||
{
|
{
|
||||||
if (_lastTheme == ActualTheme)
|
if (_lastTheme == ActualTheme)
|
||||||
@@ -150,10 +159,7 @@ public partial class IconBox : ContentControl
|
|||||||
|
|
||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
if (SourceKey is not null)
|
UpdateSourceKey(this, SourceKey);
|
||||||
{
|
|
||||||
UpdateSourceKey(this, SourceKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
@@ -170,8 +176,10 @@ public partial class IconBox : ContentControl
|
|||||||
self.Padding = default;
|
self.Padding = default;
|
||||||
break;
|
break;
|
||||||
case FontIconSource fontIcon:
|
case FontIconSource fontIcon:
|
||||||
|
self.UpdateLastFontSize();
|
||||||
if (self.Content is IconSourceElement iconSourceElement)
|
if (self.Content is IconSourceElement iconSourceElement)
|
||||||
{
|
{
|
||||||
|
fontIcon.FontSize = self._lastFontSize;
|
||||||
iconSourceElement.IconSource = fontIcon;
|
iconSourceElement.IconSource = fontIcon;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -190,7 +198,7 @@ public partial class IconBox : ContentControl
|
|||||||
self.Content = elem;
|
self.Content = elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.Padding = new Thickness(Math.Round(self._lastFontSize * -0.2));
|
self.UpdatePaddingForFontIcon();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case BitmapIconSource bitmapIcon:
|
case BitmapIconSource bitmapIcon:
|
||||||
@@ -206,10 +214,12 @@ public partial class IconBox : ContentControl
|
|||||||
self.Padding = default;
|
self.Padding = default;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IconSource source:
|
case IconSource source:
|
||||||
self.Content = source.CreateIconElement();
|
self.Content = source.CreateIconElement();
|
||||||
self.Padding = default;
|
self.Padding = default;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
throw new InvalidOperationException($"New value of {e.NewValue} is not of type IconSource.");
|
||||||
}
|
}
|
||||||
@@ -233,10 +243,10 @@ public partial class IconBox : ContentControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Callback(iconBox, sourceKey);
|
RequestIconFromSource(iconBox, sourceKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async void Callback(IconBox iconBox, object? sourceKey)
|
private static async void RequestIconFromSource(IconBox iconBox, object? sourceKey)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -256,17 +266,12 @@ public partial class IconBox : ContentControl
|
|||||||
// list virtualization situation, it's very possible we
|
// list virtualization situation, it's very possible we
|
||||||
// may have already been set to a new icon before we
|
// may have already been set to a new icon before we
|
||||||
// even got back from the await.
|
// even got back from the await.
|
||||||
if (eventArgs.Key != sourceKey)
|
if (!ReferenceEquals(sourceKey, iconBox.SourceKey))
|
||||||
{
|
{
|
||||||
// If the requested icon has changed, then just bail
|
// If the requested icon has changed, then just bail
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventArgs.Value == iconBox.Source)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
iconBox.Source = eventArgs.Value;
|
iconBox.Source = eventArgs.Value;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
using Microsoft.CmdPal.Core.Common.Services.Reports;
|
using Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
@@ -17,19 +18,20 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed partial class GlobalErrorHandler : IDisposable
|
internal sealed partial class GlobalErrorHandler : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ErrorReportBuilder _errorReportBuilder = new();
|
private ErrorReportBuilder? _errorReportBuilder;
|
||||||
private Options? _options;
|
private Options? _options;
|
||||||
private App? _app;
|
private App? _app;
|
||||||
|
|
||||||
// GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available.
|
// 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)
|
internal void Register(App app, Options options, IApplicationInfoService? appInfoService = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(app);
|
ArgumentNullException.ThrowIfNull(app);
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
_options = options;
|
_options = options;
|
||||||
|
|
||||||
_app = app;
|
_app = app;
|
||||||
|
_errorReportBuilder = new ErrorReportBuilder(appInfoService);
|
||||||
|
|
||||||
_app.UnhandledException += App_UnhandledException;
|
_app.UnhandledException += App_UnhandledException;
|
||||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||||
@@ -68,7 +70,7 @@ internal sealed partial class GlobalErrorHandler : IDisposable
|
|||||||
|
|
||||||
if (context == Context.MainThreadException)
|
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);
|
StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true);
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,6 @@ internal sealed partial class IconLoaderService : IIconLoaderService
|
|||||||
iconSize = DefaultIconSize;
|
iconSize = DefaultIconSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IconPathConverter.IconSourceMUX(iconString, false, fontFamily, iconSize);
|
return IconPathConverter.IconSourceMUX(iconString, fontFamily, iconSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||||
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||||
ImageOpacity="{x:Bind ViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
ImageOpacity="{x:Bind ViewModel.EffectiveImageOpacity, Mode=OneWay}"
|
||||||
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||||
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||||
IsHitTestVisible="False"
|
IsHitTestVisible="False"
|
||||||
@@ -31,6 +31,6 @@
|
|||||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||||
|
|
||||||
<pages:ShellPage />
|
<pages:ShellPage HostWindow="{x:Bind}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</winuiex:WindowEx>
|
</winuiex:WindowEx>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ using Windows.ApplicationModel.Activation;
|
|||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
|
using Windows.UI;
|
||||||
using Windows.Win32;
|
using Windows.Win32;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.Graphics.Dwm;
|
using Windows.Win32.Graphics.Dwm;
|
||||||
@@ -53,7 +54,9 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
IRecipient<ErrorOccurredMessage>,
|
IRecipient<ErrorOccurredMessage>,
|
||||||
IRecipient<DragStartedMessage>,
|
IRecipient<DragStartedMessage>,
|
||||||
IRecipient<DragCompletedMessage>,
|
IRecipient<DragCompletedMessage>,
|
||||||
IDisposable
|
IRecipient<ToggleDevRibbonMessage>,
|
||||||
|
IDisposable,
|
||||||
|
IHostWindow
|
||||||
{
|
{
|
||||||
[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", "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_")]
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||||
@@ -80,15 +83,21 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
private int _sessionErrorCount;
|
private int _sessionErrorCount;
|
||||||
|
|
||||||
private DesktopAcrylicController? _acrylicController;
|
private DesktopAcrylicController? _acrylicController;
|
||||||
|
private MicaController? _micaController;
|
||||||
private SystemBackdropConfiguration? _configurationSource;
|
private SystemBackdropConfiguration? _configurationSource;
|
||||||
|
private bool _isUpdatingBackdrop;
|
||||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||||
|
|
||||||
private WindowPosition _currentWindowPosition = new();
|
private WindowPosition _currentWindowPosition = new();
|
||||||
|
|
||||||
private bool _preventHideWhenDeactivated;
|
private bool _preventHideWhenDeactivated;
|
||||||
|
|
||||||
|
private DevRibbon? _devRibbon;
|
||||||
|
|
||||||
private MainWindowViewModel ViewModel { get; }
|
private MainWindowViewModel ViewModel { get; }
|
||||||
|
|
||||||
|
public bool IsVisibleToUser { get; private set; } = true;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -109,7 +118,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetAcrylic();
|
InitializeBackdropSupport();
|
||||||
|
|
||||||
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
|
||||||
|
|
||||||
@@ -133,6 +142,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
|
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<ToggleDevRibbonMessage>(this);
|
||||||
|
|
||||||
// Hide our titlebar.
|
// Hide our titlebar.
|
||||||
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
||||||
@@ -158,7 +168,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
|
||||||
|
|
||||||
// Make sure that we update the acrylic theme when the OS theme changes
|
// Make sure that we update the acrylic theme when the OS theme changes
|
||||||
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
|
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
|
||||||
|
|
||||||
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
|
// 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", () =>
|
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
|
||||||
@@ -185,7 +195,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||||
{
|
{
|
||||||
UpdateAcrylic();
|
UpdateBackdrop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||||
@@ -206,7 +216,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
// Add dev ribbon if enabled
|
// Add dev ribbon if enabled
|
||||||
if (!BuildInfo.IsCiBuild)
|
if (!BuildInfo.IsCiBuild)
|
||||||
{
|
{
|
||||||
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
|
_devRibbon = new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) };
|
||||||
|
RootElement.Children.Add(_devRibbon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,48 +291,170 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetAcrylic()
|
private void InitializeBackdropSupport()
|
||||||
{
|
{
|
||||||
if (DesktopAcrylicController.IsSupported())
|
if (DesktopAcrylicController.IsSupported() || MicaController.IsSupported())
|
||||||
{
|
{
|
||||||
// Hooking up the policy object.
|
|
||||||
_configurationSource = new SystemBackdropConfiguration
|
_configurationSource = new SystemBackdropConfiguration
|
||||||
{
|
{
|
||||||
// Initial configuration state.
|
|
||||||
IsInputActive = true,
|
IsInputActive = true,
|
||||||
};
|
};
|
||||||
UpdateAcrylic();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAcrylic()
|
private void UpdateBackdrop()
|
||||||
{
|
{
|
||||||
|
// 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
|
try
|
||||||
{
|
{
|
||||||
if (_acrylicController != null)
|
switch (config.ControllerKind)
|
||||||
{
|
{
|
||||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
case BackdropControllerKind.Solid:
|
||||||
_acrylicController.Dispose();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError("Failed to update backdrop", 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)
|
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||||
@@ -575,17 +708,14 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
|
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsVisibleToUser = false;
|
||||||
|
}
|
||||||
|
|
||||||
wasCloaked = hr.Succeeded;
|
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;
|
return wasCloaked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +725,7 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
{
|
{
|
||||||
BOOL value = false;
|
BOOL value = false;
|
||||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
||||||
|
IsVisibleToUser = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,12 +768,8 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
|
|
||||||
private void DisposeAcrylic()
|
private void DisposeAcrylic()
|
||||||
{
|
{
|
||||||
if (_acrylicController is not null)
|
CleanupBackdropControllers();
|
||||||
{
|
_configurationSource = null!;
|
||||||
_acrylicController.Dispose();
|
|
||||||
_acrylicController = null!;
|
|
||||||
_configurationSource = null!;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates our window s.t. the top of the window is draggable.
|
// Updates our window s.t. the top of the window is draggable.
|
||||||
@@ -1012,6 +1139,11 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
DisposeAcrylic();
|
DisposeAcrylic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Receive(ToggleDevRibbonMessage message)
|
||||||
|
{
|
||||||
|
_devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(DragStartedMessage message)
|
public void Receive(DragStartedMessage message)
|
||||||
{
|
{
|
||||||
_preventHideWhenDeactivated = true;
|
_preventHideWhenDeactivated = true;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// 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.Messages;
|
||||||
|
|
||||||
|
public record ToggleDevRibbonMessage;
|
||||||
@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages;
|
|||||||
using Microsoft.CmdPal.UI.Events;
|
using Microsoft.CmdPal.UI.Events;
|
||||||
using Microsoft.CmdPal.UI.Helpers;
|
using Microsoft.CmdPal.UI.Helpers;
|
||||||
using Microsoft.CmdPal.UI.Messages;
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
|
using Microsoft.CmdPal.UI.Services;
|
||||||
using Microsoft.CmdPal.UI.Settings;
|
using Microsoft.CmdPal.UI.Settings;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@@ -72,6 +73,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public IHostWindow? HostWindow { get; set; }
|
||||||
|
|
||||||
public ShellPage()
|
public ShellPage()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
@@ -435,7 +438,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
|
|
||||||
if (!RootFrame.CanGoBack)
|
if (!RootFrame.CanGoBack)
|
||||||
{
|
{
|
||||||
ViewModel.GoHome();
|
ViewModel.GoHome(withAnimation, focusSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (focusSearch)
|
if (focusSearch)
|
||||||
@@ -531,6 +534,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
|
|
||||||
if (shouldSearchBoxBeVisible || page is not ContentPage)
|
if (shouldSearchBoxBeVisible || page is not ContentPage)
|
||||||
{
|
{
|
||||||
|
if (HostWindow?.IsVisibleToUser != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible;
|
ViewModel.IsSearchBoxVisible = shouldSearchBoxBeVisible;
|
||||||
SearchBox.Focus(FocusState.Programmatic);
|
SearchBox.Focus(FocusState.Programmatic);
|
||||||
SearchBox.SelectSearch();
|
SearchBox.SelectSearch();
|
||||||
@@ -547,6 +555,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (HostWindow?.IsVisibleToUser != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await page.DispatcherQueue.EnqueueAsync(
|
await page.DispatcherQueue.EnqueueAsync(
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
@@ -556,6 +569,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (HostWindow?.IsVisibleToUser != true)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||||
{
|
{
|
||||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
using Microsoft.CmdPal.Core.Common.Services;
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Core.ViewModels;
|
using Microsoft.CmdPal.Core.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||||
@@ -23,13 +24,13 @@ internal sealed class PowerToysRootPageService : IRootPageService
|
|||||||
private IExtensionWrapper? _activeExtension;
|
private IExtensionWrapper? _activeExtension;
|
||||||
private Lazy<MainListPage> _mainListPage;
|
private Lazy<MainListPage> _mainListPage;
|
||||||
|
|
||||||
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel, IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||||
{
|
{
|
||||||
_tlcManager = topLevelCommandManager;
|
_tlcManager = topLevelCommandManager;
|
||||||
|
|
||||||
_mainListPage = new Lazy<MainListPage>(() =>
|
_mainListPage = new Lazy<MainListPage>(() =>
|
||||||
{
|
{
|
||||||
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel);
|
return new MainListPage(_tlcManager, settings, aliasManager, appStateModel, fuzzyMatcherProvider);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
using Microsoft.CmdPal.UI.Events;
|
using Microsoft.CmdPal.UI.Events;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
@@ -65,6 +66,25 @@ internal sealed class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
|
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());
|
PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted());
|
||||||
|
|
||||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using CommunityToolkit.WinUI.Helpers;
|
using CommunityToolkit.WinUI.Helpers;
|
||||||
using Microsoft.CmdPal.UI.Helpers;
|
using Microsoft.CmdPal.UI.Helpers;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
@@ -34,7 +35,7 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
|
|||||||
_uiSettings = uiSettings;
|
_uiSettings = uiSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
public BackdropParameters GetBackdropParameters(ThemeContext context)
|
||||||
{
|
{
|
||||||
var isLight = context.Theme == ElementTheme.Light ||
|
var isLight = context.Theme == ElementTheme.Light ||
|
||||||
(context.Theme == ElementTheme.Default &&
|
(context.Theme == ElementTheme.Default &&
|
||||||
@@ -53,7 +54,26 @@ internal sealed class ColorfulThemeProvider : IThemeProvider
|
|||||||
var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
|
var colorIntensity = isLight ? 0.6f * colorIntensityUser : colorIntensityUser;
|
||||||
var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
|
var effectiveBgColor = ColorBlender.Blend(baseColor, blended, colorIntensity);
|
||||||
|
|
||||||
return new AcrylicBackdropParameters(effectiveBgColor, effectiveBgColor, 0.8f, 0.8f);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ColorBlender
|
private static class ColorBlender
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace Microsoft.CmdPal.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; }
|
||||||
|
}
|
||||||
@@ -8,14 +8,14 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
|
|||||||
namespace Microsoft.CmdPal.UI.Services;
|
namespace Microsoft.CmdPal.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides theme identification, resource path resolution, and creation of acrylic
|
/// Provides theme identification, resource path resolution, and creation of backdrop
|
||||||
/// backdrop parameters based on the current <see cref="ThemeContext"/>.
|
/// parameters based on the current <see cref="ThemeContext"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Implementations should expose a stable <see cref="ThemeKey"/> and a valid XAML resource
|
/// Implementations should expose a stable <see cref="ThemeKey"/> and a valid XAML resource
|
||||||
/// dictionary path via <see cref="ResourcePath"/>. The
|
/// dictionary path via <see cref="ResourcePath"/>. The
|
||||||
/// <see cref="GetAcrylicBackdrop(ThemeContext)"/> method computes
|
/// <see cref="GetBackdropParameters(ThemeContext)"/> method computes
|
||||||
/// <see cref="AcrylicBackdropParameters"/> using the supplied theme context.
|
/// <see cref="BackdropParameters"/> using the supplied theme context.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal interface IThemeProvider
|
internal interface IThemeProvider
|
||||||
{
|
{
|
||||||
@@ -30,9 +30,9 @@ internal interface IThemeProvider
|
|||||||
string ResourcePath { get; }
|
string ResourcePath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates acrylic backdrop parameters based on the provided theme context.
|
/// Creates backdrop parameters based on the provided theme context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The current theme context, including theme, tint, and optional background details.</param>
|
/// <param name="context">The current theme context, including theme, tint, transparency mode, and optional background details.</param>
|
||||||
/// <returns>The computed <see cref="AcrylicBackdropParameters"/> for the backdrop.</returns>
|
/// <returns>The computed <see cref="BackdropParameters"/> for the backdrop.</returns>
|
||||||
AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context);
|
BackdropParameters GetBackdropParameters(ThemeContext context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
@@ -28,16 +29,28 @@ internal sealed class NormalThemeProvider : IThemeProvider
|
|||||||
|
|
||||||
public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
|
public string ResourcePath => "ms-appx:///Styles/Theme.Normal.xaml";
|
||||||
|
|
||||||
public AcrylicBackdropParameters GetAcrylicBackdrop(ThemeContext context)
|
public BackdropParameters GetBackdropParameters(ThemeContext context)
|
||||||
{
|
{
|
||||||
var isLight = context.Theme == ElementTheme.Light ||
|
var isLight = context.Theme == ElementTheme.Light ||
|
||||||
(context.Theme == ElementTheme.Default &&
|
(context.Theme == ElementTheme.Default &&
|
||||||
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
|
_uiSettings.GetColorValue(UIColorType.Background).R > 128);
|
||||||
|
|
||||||
return new AcrylicBackdropParameters(
|
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(
|
||||||
TintColor: isLight ? LightBaseColor : DarkBaseColor,
|
TintColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||||
FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
|
FallbackColor: isLight ? LightBaseColor : DarkBaseColor,
|
||||||
TintOpacity: 0.5f,
|
EffectiveOpacity: effectiveOpacity,
|
||||||
LuminosityOpacity: isLight ? 0.9f : 0.96f);
|
EffectiveLuminosityOpacity: effectiveLuminosityOpacity,
|
||||||
|
Style: backdropStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Services;
|
namespace Microsoft.CmdPal.UI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input parameters for theme computation, passed to theme providers.
|
||||||
|
/// </summary>
|
||||||
internal sealed record ThemeContext
|
internal sealed record ThemeContext
|
||||||
{
|
{
|
||||||
public ElementTheme Theme { get; init; }
|
public ElementTheme Theme { get; init; }
|
||||||
@@ -21,4 +25,8 @@ internal sealed record ThemeContext
|
|||||||
public double BackgroundImageOpacity { get; init; }
|
public double BackgroundImageOpacity { get; init; }
|
||||||
|
|
||||||
public int? ColorIntensity { get; init; }
|
public int? ColorIntensity { get; init; }
|
||||||
|
|
||||||
|
public BackdropStyle? BackdropStyle { get; init; }
|
||||||
|
|
||||||
|
public float BackdropOpacity { get; init; } = 1.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,13 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// provider selection
|
// provider selection
|
||||||
var intensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
|
var themeColorIntensity = Math.Clamp(_settings.CustomThemeColorIntensity, 0, 100);
|
||||||
IThemeProvider provider = intensity > 0 && _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
var imageTintIntensity = Math.Clamp(_settings.BackgroundImageTintIntensity, 0, 100);
|
||||||
? _colorfulThemeProvider
|
var effectiveColorIntensity = _settings.ColorizationMode == ColorizationMode.Image
|
||||||
: _normalThemeProvider;
|
? imageTintIntensity
|
||||||
|
: themeColorIntensity;
|
||||||
|
|
||||||
|
IThemeProvider provider = UseColorfulProvider(effectiveColorIntensity) ? _colorfulThemeProvider : _normalThemeProvider;
|
||||||
|
|
||||||
// Calculate values
|
// Calculate values
|
||||||
var tint = _settings.ColorizationMode switch
|
var tint = _settings.ColorizationMode switch
|
||||||
@@ -96,32 +99,39 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
|||||||
};
|
};
|
||||||
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
|
var opacity = Math.Clamp(_settings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||||
|
|
||||||
// create context and offload to actual theme provider
|
// create input and offload to actual theme provider
|
||||||
var context = new ThemeContext
|
var context = new ThemeContext
|
||||||
{
|
{
|
||||||
Tint = tint,
|
Tint = tint,
|
||||||
ColorIntensity = intensity,
|
ColorIntensity = effectiveColorIntensity,
|
||||||
Theme = effectiveTheme,
|
Theme = effectiveTheme,
|
||||||
BackgroundImageSource = imageSource,
|
BackgroundImageSource = imageSource,
|
||||||
BackgroundImageStretch = stretch,
|
BackgroundImageStretch = stretch,
|
||||||
BackgroundImageOpacity = opacity,
|
BackgroundImageOpacity = opacity,
|
||||||
|
BackdropStyle = _settings.BackdropStyle,
|
||||||
|
BackdropOpacity = Math.Clamp(_settings.BackdropOpacity, 0, 100) / 100f,
|
||||||
};
|
};
|
||||||
var backdrop = provider.GetAcrylicBackdrop(context);
|
var backdrop = provider.GetBackdropParameters(context);
|
||||||
var blur = _settings.BackgroundImageBlurAmount;
|
var blur = _settings.BackgroundImageBlurAmount;
|
||||||
var brightness = _settings.BackgroundImageBrightness;
|
var brightness = _settings.BackgroundImageBrightness;
|
||||||
|
|
||||||
// Create public snapshot (no provider!)
|
// Create public snapshot (no provider!)
|
||||||
|
var hasColorization = effectiveColorIntensity > 0
|
||||||
|
&& _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||||
|
|
||||||
var snapshot = new ThemeSnapshot
|
var snapshot = new ThemeSnapshot
|
||||||
{
|
{
|
||||||
Tint = tint,
|
Tint = tint,
|
||||||
TintIntensity = intensity / 100f,
|
TintIntensity = effectiveColorIntensity / 100f,
|
||||||
Theme = effectiveTheme,
|
Theme = effectiveTheme,
|
||||||
BackgroundImageSource = imageSource,
|
BackgroundImageSource = imageSource,
|
||||||
BackgroundImageStretch = stretch,
|
BackgroundImageStretch = stretch,
|
||||||
BackgroundImageOpacity = opacity,
|
BackgroundImageOpacity = opacity,
|
||||||
BackdropParameters = backdrop,
|
BackdropParameters = backdrop,
|
||||||
|
BackdropOpacity = context.BackdropOpacity,
|
||||||
BlurAmount = blur,
|
BlurAmount = blur,
|
||||||
BackgroundBrightness = brightness / 100f,
|
BackgroundBrightness = brightness / 100f,
|
||||||
|
HasColorization = hasColorization,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bundle with provider for internal use
|
// Bundle with provider for internal use
|
||||||
@@ -138,6 +148,12 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
|||||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
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)
|
private static BitmapImage? LoadImageSafe(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@@ -195,13 +211,15 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
|||||||
{
|
{
|
||||||
Tint = Colors.Transparent,
|
Tint = Colors.Transparent,
|
||||||
Theme = ElementTheme.Light,
|
Theme = ElementTheme.Light,
|
||||||
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
BackdropParameters = new BackdropParameters(Colors.Black, Colors.Black, EffectiveOpacity: 0.5f, EffectiveLuminosityOpacity: 0.5f),
|
||||||
|
BackdropOpacity = 1.0f,
|
||||||
BackgroundImageOpacity = 1,
|
BackgroundImageOpacity = 1,
|
||||||
BackgroundImageSource = null,
|
BackgroundImageSource = null,
|
||||||
BackgroundImageStretch = Stretch.Fill,
|
BackgroundImageStretch = Stretch.Fill,
|
||||||
BlurAmount = 0,
|
BlurAmount = 0,
|
||||||
TintIntensity = 1.0f,
|
TintIntensity = 1.0f,
|
||||||
BackgroundBrightness = 0,
|
BackgroundBrightness = 0,
|
||||||
|
HasColorization = false,
|
||||||
},
|
},
|
||||||
Provider = _normalThemeProvider,
|
Provider = _normalThemeProvider,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,18 +22,50 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Spacing="{StaticResource SettingsCardSpacing}">
|
Spacing="{StaticResource SettingsCardSpacing}">
|
||||||
|
|
||||||
<ptControls:ScreenPreview Margin="0,0,0,16" HorizontalAlignment="Left">
|
<StackPanel
|
||||||
<ptControls:CommandPalettePreview
|
Margin="0,0,0,16"
|
||||||
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
|
HorizontalAlignment="Left"
|
||||||
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
|
Orientation="Horizontal"
|
||||||
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
|
Spacing="16">
|
||||||
PreviewBackgroundImageFit="{x:Bind ViewModel.Appearance.BackgroundImageFit, Mode=OneWay}"
|
<ptControls:ScreenPreview>
|
||||||
PreviewBackgroundImageSource="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageSource, Mode=OneWay}"
|
<ptControls:CommandPalettePreview
|
||||||
PreviewBackgroundImageTint="{x:Bind ViewModel.Appearance.EffectiveThemeColor, Mode=OneWay}"
|
PreviewBackdropStyle="{x:Bind ViewModel.Appearance.EffectiveBackdropStyle, Mode=OneWay}"
|
||||||
PreviewBackgroundImageTintIntensity="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=OneWay}"
|
PreviewBackgroundColor="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintColor, Mode=OneWay}"
|
||||||
PreviewBackgroundOpacity="{x:Bind ViewModel.Appearance.EffectiveBackdrop.TintOpacity, Mode=OneWay}"
|
PreviewBackgroundImageBlurAmount="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBlurAmount, Mode=OneWay}"
|
||||||
RequestedTheme="{x:Bind ViewModel.Appearance.EffectiveTheme, Mode=OneWay}" />
|
PreviewBackgroundImageBrightness="{x:Bind ViewModel.Appearance.EffectiveBackgroundImageBrightness, Mode=OneWay}"
|
||||||
</ptControls:ScreenPreview>
|
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="" />
|
||||||
|
<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="" />
|
||||||
|
<TextBlock x:Uid="Settings_AppearancePage_ResetAppearanceButton_Text" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
|
||||||
@@ -62,19 +94,67 @@
|
|||||||
</ComboBox>
|
</ComboBox>
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
|
|
||||||
|
<controls:SettingsExpander
|
||||||
|
x:Uid="Settings_GeneralPage_BackdropStyle_SettingsCard"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
|
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
|
<controls:SettingsExpander
|
||||||
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
|
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
|
||||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}"
|
||||||
IsExpanded="{x:Bind ViewModel.Appearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
IsExpanded="{x:Bind ViewModel.Appearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||||
<ComboBox
|
<Grid>
|
||||||
x:Uid="Settings_GeneralPage_ColorizationMode"
|
<ComboBox
|
||||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
x:Uid="Settings_GeneralPage_ColorizationMode"
|
||||||
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}">
|
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}"
|
||||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}">
|
||||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||||
</ComboBox>
|
<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>
|
||||||
<controls:SettingsExpander.Items>
|
<controls:SettingsExpander.Items>
|
||||||
<!-- none -->
|
<!-- none -->
|
||||||
<controls:SettingsCard
|
<controls:SettingsCard
|
||||||
@@ -155,7 +235,7 @@
|
|||||||
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
|
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
|
||||||
SelectedColor="{x:Bind ViewModel.Appearance.ThemeColor, Mode=TwoWay}" />
|
SelectedColor="{x:Bind ViewModel.Appearance.ThemeColor, Mode=TwoWay}" />
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsColorIntensityVisible, Mode=OneWay}">
|
||||||
<Slider
|
<Slider
|
||||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||||
Maximum="100"
|
Maximum="100"
|
||||||
@@ -163,9 +243,17 @@
|
|||||||
StepFrequency="1"
|
StepFrequency="1"
|
||||||
Value="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=TwoWay}" />
|
Value="{x:Bind ViewModel.Appearance.ColorIntensity, Mode=TwoWay}" />
|
||||||
</controls:SettingsCard>
|
</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 background image properties -->
|
<!-- Reset appearance properties -->
|
||||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsResetButtonVisible, Mode=OneWay}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using ManagedCommon;
|
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;
|
||||||
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI;
|
using Microsoft.UI;
|
||||||
@@ -12,6 +17,7 @@ using Microsoft.UI.Xaml;
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Documents;
|
using Microsoft.UI.Xaml.Documents;
|
||||||
using Microsoft.Windows.Storage.Pickers;
|
using Microsoft.Windows.Storage.Pickers;
|
||||||
|
using Windows.Win32.Foundation;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Settings;
|
namespace Microsoft.CmdPal.UI.Settings;
|
||||||
|
|
||||||
@@ -86,4 +92,9 @@ public sealed partial class AppearancePage : Page
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenCommandPalette_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(string.Empty, HWND.Null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using ManagedCommon;
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
using Microsoft.CmdPal.UI.Helpers;
|
using Microsoft.CmdPal.UI.Helpers;
|
||||||
using Microsoft.CmdPal.UI.ViewModels;
|
using Microsoft.CmdPal.UI.ViewModels;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI.Settings;
|
namespace Microsoft.CmdPal.UI.Settings;
|
||||||
|
|
||||||
@@ -19,6 +17,7 @@ public sealed partial class GeneralPage : Page
|
|||||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||||
|
|
||||||
private readonly SettingsViewModel? viewModel;
|
private readonly SettingsViewModel? viewModel;
|
||||||
|
private readonly IApplicationInfoService _appInfoService;
|
||||||
|
|
||||||
public GeneralPage()
|
public GeneralPage()
|
||||||
{
|
{
|
||||||
@@ -27,6 +26,7 @@ public sealed partial class GeneralPage : Page
|
|||||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||||
|
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
|
||||||
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
viewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,55 +35,8 @@ public sealed partial class GeneralPage : Page
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
|
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
|
||||||
if (!TryGetPackagedVersion(out var version) && !TryGetAssemblyVersion(out version))
|
var version = _appInfoService.AppVersion;
|
||||||
{
|
|
||||||
version = "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Format(CultureInfo.CurrentCulture, versionNo, 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,13 @@
|
|||||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
|
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
|
||||||
</controls:SettingsCard>
|
</controls:SettingsCard>
|
||||||
|
<controls:SettingsCard
|
||||||
|
x:Name="ToggleDevRibbonVisibilitySettingsCard"
|
||||||
|
Description="This is only temporary and state is not saved"
|
||||||
|
Header="Toggle dev ribbon visibility"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||||
|
<Button Click="ToggleDevRibbonClicked" Content="Toggle dev ribbon" />
|
||||||
|
</controls:SettingsCard>
|
||||||
|
|
||||||
<!-- Data Section -->
|
<!-- Data Section -->
|
||||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
|
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
|
using Microsoft.CmdPal.UI.Messages;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Windows.System;
|
using Windows.System;
|
||||||
using Page = Microsoft.UI.Xaml.Controls.Page;
|
using Page = Microsoft.UI.Xaml.Controls.Page;
|
||||||
@@ -15,9 +19,13 @@ namespace Microsoft.CmdPal.UI.Settings;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class InternalPage : Page
|
public sealed partial class InternalPage : Page
|
||||||
{
|
{
|
||||||
|
private readonly IApplicationInfoService _appInfoService;
|
||||||
|
|
||||||
public InternalPage()
|
public InternalPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
_appInfoService = App.Current.Services.GetRequiredService<IApplicationInfoService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
|
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -46,7 +54,7 @@ public sealed partial class InternalPage : Page
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
|
var logFolderPath = _appInfoService.LogDirectory;
|
||||||
if (Directory.Exists(logFolderPath))
|
if (Directory.Exists(logFolderPath))
|
||||||
{
|
{
|
||||||
await Launcher.LaunchFolderPathAsync(logFolderPath);
|
await Launcher.LaunchFolderPathAsync(logFolderPath);
|
||||||
@@ -78,7 +86,7 @@ public sealed partial class InternalPage : Page
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
var directory = _appInfoService.ConfigDirectory;
|
||||||
if (Directory.Exists(directory))
|
if (Directory.Exists(directory))
|
||||||
{
|
{
|
||||||
await Launcher.LaunchFolderPathAsync(directory);
|
await Launcher.LaunchFolderPathAsync(directory);
|
||||||
@@ -89,4 +97,9 @@ public sealed partial class InternalPage : Page
|
|||||||
Logger.LogError("Failed to open directory in Explorer", ex);
|
Logger.LogError("Failed to open directory in Explorer", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleDevRibbonClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new ToggleDevRibbonMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</resheader>
|
<resheader name="version">2.0</resheader>
|
||||||
@@ -26,36 +26,36 @@
|
|||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
value : The object must be serialized into a byte array
|
value : The object must be serialized into a byte array
|
||||||
: using a System.ComponentModel.TypeConverter
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
@@ -577,6 +577,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<data name="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
<data name="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||||
<value>Color intensity</value>
|
<value>Color intensity</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_GeneralPage_ImageTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||||
|
<value>Color intensity</value>
|
||||||
|
</data>
|
||||||
<data name="OptionalColorPickerButton_UnsetTextBlock.Text" xml:space="preserve">
|
<data name="OptionalColorPickerButton_UnsetTextBlock.Text" xml:space="preserve">
|
||||||
<value>Choose color</value>
|
<value>Choose color</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -668,7 +671,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<value>Background image</value>
|
<value>Background image</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_GeneralPage_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
<data name="Settings_GeneralPage_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||||
<value>No settings</value>
|
<value>No additional settings are available.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Header" xml:space="preserve">
|
<data name="Settings_GeneralPage_Background_SettingsExpander.Header" xml:space="preserve">
|
||||||
<value>Background</value>
|
<value>Background</value>
|
||||||
@@ -676,6 +679,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<data name="Settings_GeneralPage_Background_SettingsExpander.Description" xml:space="preserve">
|
<data name="Settings_GeneralPage_Background_SettingsExpander.Description" xml:space="preserve">
|
||||||
<value>Choose a custom background color or image</value>
|
<value>Choose a custom background color or image</value>
|
||||||
</data>
|
</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">
|
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||||
<value>System accent color</value>
|
<value>System accent color</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -692,7 +698,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<value>Restore defaults</value>
|
<value>Restore defaults</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_GeneralPage_Background_ResetImagePropertiesButton.Content" xml:space="preserve">
|
<data name="Settings_GeneralPage_Background_ResetImagePropertiesButton.Content" xml:space="preserve">
|
||||||
<value>Reset</value>
|
<value>Reset image settings</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1.Text" xml:space="preserve">
|
<data name="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1.Text" xml:space="preserve">
|
||||||
<value>Change the system accent in Windows Settings:</value>
|
<value>Change the system accent in Windows Settings:</value>
|
||||||
@@ -727,4 +733,45 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
|||||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||||
<value>Version {0}</value>
|
<value>Version {0}</value>
|
||||||
</data>
|
</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>
|
||||||
</root>
|
</root>
|
||||||
@@ -107,6 +107,12 @@ internal sealed partial class DevRibbonViewModel : ObservableObject
|
|||||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal"));
|
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Internal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleDevRibbonVisibility()
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new ToggleDevRibbonMessage());
|
||||||
|
}
|
||||||
|
|
||||||
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
|
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
|
||||||
{
|
{
|
||||||
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
|
||||||
|
|||||||
@@ -94,40 +94,49 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
|
// - <TIconSource>: The type of IconSource (MUX, WUX) to generate.
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// - path: the full, expanded path to the icon.
|
// - path: the full, expanded path to the icon.
|
||||||
|
// - targetSize: the target size for decoding/rasterizing the icon.
|
||||||
// Return Value:
|
// Return Value:
|
||||||
// - An IconElement with its IconSource set, if possible.
|
// - An IconElement with its IconSource set, if possible.
|
||||||
template<typename TIconSource>
|
template<typename TIconSource>
|
||||||
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome)
|
TIconSource _getColoredBitmapIcon(const winrt::hstring& path, int targetSize)
|
||||||
{
|
{
|
||||||
// FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters.
|
// 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.
|
// 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)
|
||||||
{
|
{
|
||||||
try
|
return nullptr;
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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 };
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CATCH_LOG();
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,14 +167,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
// Return Value:
|
// Return Value:
|
||||||
// - An IconElement with its IconSource set, if possible.
|
// - An IconElement with its IconSource set, if possible.
|
||||||
template<typename TIconSource>
|
template<typename TIconSource>
|
||||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
TIconSource _getIconSource(const winrt::hstring& iconPath, const winrt::hstring& fontFamily, const int targetSize)
|
||||||
{
|
{
|
||||||
TIconSource iconSource{ nullptr };
|
TIconSource iconSource{ nullptr };
|
||||||
|
|
||||||
if (iconPath.size() != 0)
|
if (iconPath.size() != 0)
|
||||||
{
|
{
|
||||||
const auto expandedIconPath{ _expandIconPath(iconPath) };
|
const auto expandedIconPath{ _expandIconPath(iconPath) };
|
||||||
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, monochrome);
|
iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, targetSize);
|
||||||
|
|
||||||
// If we fail to set the icon source using the "icon" as a path,
|
// If we fail to set the icon source using the "icon" as a path,
|
||||||
// let's try it as a symbol/emoji.
|
// let's try it as a symbol/emoji.
|
||||||
@@ -235,9 +244,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, const winrt::hstring& fontFamily, const int targetSize)
|
||||||
{
|
{
|
||||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
|
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, fontFamily, targetSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||||
@@ -352,7 +361,6 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
}
|
}
|
||||||
|
|
||||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||||
const bool monochrome,
|
|
||||||
const winrt::hstring& fontFamily,
|
const winrt::hstring& fontFamily,
|
||||||
const int targetSize)
|
const int targetSize)
|
||||||
{
|
{
|
||||||
@@ -360,7 +368,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||||
if (!indexOpt.has_value())
|
if (!indexOpt.has_value())
|
||||||
{
|
{
|
||||||
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
|
return _IconSourceMUX(iconPath, fontFamily, targetSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||||
@@ -374,13 +382,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
|||||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) {
|
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) {
|
||||||
return IconMUX(iconPath, 24);
|
return IconMUX(iconPath, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize)
|
Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize)
|
||||||
{
|
{
|
||||||
std::wstring_view iconPathWithoutIndex;
|
std::wstring_view iconPathWithoutIndex;
|
||||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||||
if (!indexOpt.has_value())
|
if (!indexOpt.has_value())
|
||||||
{
|
{
|
||||||
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
|
auto source = IconSourceMUX(iconPath, L"", targetSize);
|
||||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||||
icon.IconSource(source);
|
icon.IconSource(source);
|
||||||
return icon;
|
return icon;
|
||||||
|
|||||||
@@ -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::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(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, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24);
|
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, 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);
|
||||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
|||||||
{
|
{
|
||||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
||||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily, Int32 targetSize);
|
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, String fontFamily, Int32 targetSize);
|
||||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,7 +201,7 @@
|
|||||||
<None Include="Microsoft.Terminal.UI.def" />
|
<None Include="Microsoft.Terminal.UI.def" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutDir>
|
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\</OutDir>
|
||||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// 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 Random’s 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||||
@@ -43,38 +44,34 @@ public partial class MainListPageResultFactoryTests
|
|||||||
public override string ToString() => Title;
|
public override string ToString() => Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Scored<IListItem> S(string title, int score)
|
private static RoScored<IListItem> S(string title, int score)
|
||||||
{
|
{
|
||||||
return new Scored<IListItem>
|
return new RoScored<IListItem>(score: score, item: new MockListItem { Title = title });
|
||||||
{
|
|
||||||
Score = score,
|
|
||||||
Item = new MockListItem { Title = title },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Merge_PrioritizesListsCorrectly()
|
public void Merge_PrioritizesListsCorrectly()
|
||||||
{
|
{
|
||||||
var filtered = new List<Scored<IListItem>>
|
var filtered = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("F1", 100),
|
S("F1", 100),
|
||||||
S("F2", 50),
|
S("F2", 50),
|
||||||
};
|
};
|
||||||
|
|
||||||
var scoredFallback = new List<Scored<IListItem>>
|
var scoredFallback = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("SF1", 100),
|
S("SF1", 100),
|
||||||
S("SF2", 60),
|
S("SF2", 60),
|
||||||
};
|
};
|
||||||
|
|
||||||
var apps = new List<Scored<IListItem>>
|
var apps = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("A1", 100),
|
S("A1", 100),
|
||||||
S("A2", 55),
|
S("A2", 55),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallbacks are not scored.
|
// Fallbacks are not scored.
|
||||||
var fallbacks = new List<Scored<IListItem>>
|
var fallbacks = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("FB1", 0),
|
S("FB1", 0),
|
||||||
S("FB2", 0),
|
S("FB2", 0),
|
||||||
@@ -104,7 +101,7 @@ public partial class MainListPageResultFactoryTests
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Merge_AppliesAppLimit()
|
public void Merge_AppliesAppLimit()
|
||||||
{
|
{
|
||||||
var apps = new List<Scored<IListItem>>
|
var apps = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("A1", 100),
|
S("A1", 100),
|
||||||
S("A2", 90),
|
S("A2", 90),
|
||||||
@@ -126,7 +123,7 @@ public partial class MainListPageResultFactoryTests
|
|||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void Merge_FiltersEmptyFallbacks()
|
public void Merge_FiltersEmptyFallbacks()
|
||||||
{
|
{
|
||||||
var fallbacks = new List<Scored<IListItem>>
|
var fallbacks = new List<RoScored<IListItem>>
|
||||||
{
|
{
|
||||||
S("FB1", 0),
|
S("FB1", 0),
|
||||||
S("FB3", 0),
|
S("FB3", 0),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.CmdPal.Core.Common.Text;
|
||||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||||
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
using Microsoft.CmdPal.UI.ViewModels.MainPage;
|
||||||
using Microsoft.CommandPalette.Extensions;
|
using Microsoft.CommandPalette.Extensions;
|
||||||
@@ -263,10 +264,12 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
};
|
};
|
||||||
|
|
||||||
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
var history = CreateHistory(items.Reverse<ListItemMock>().ToList());
|
||||||
|
var fuzzyMatcher = CreateMatcher();
|
||||||
|
var q = fuzzyMatcher.PrecomputeQuery("C");
|
||||||
|
|
||||||
var scoreA = MainListPage.ScoreTopLevelItem("C", items[0], history);
|
var scoreA = MainListPage.ScoreTopLevelItem(q, items[0], history, fuzzyMatcher);
|
||||||
var scoreB = MainListPage.ScoreTopLevelItem("C", items[1], history);
|
var scoreB = MainListPage.ScoreTopLevelItem(q, items[1], history, fuzzyMatcher);
|
||||||
var scoreC = MainListPage.ScoreTopLevelItem("C", items[2], history);
|
var scoreC = MainListPage.ScoreTopLevelItem(q, items[2], history, fuzzyMatcher);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// All of these equally match the query, and they're all in the same bucket,
|
// All of these equally match the query, and they're all in the same bucket,
|
||||||
@@ -296,6 +299,11 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IPrecomputedFuzzyMatcher CreateMatcher()
|
||||||
|
{
|
||||||
|
return new PrecomputedFuzzyMatcher(new PrecomputedFuzzyMatcherOptions());
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record ScoredItem(ListItemMock Item, int Score)
|
private sealed record ScoredItem(ListItemMock Item, int Score)
|
||||||
{
|
{
|
||||||
public string Title => Item.Title;
|
public string Title => Item.Title;
|
||||||
@@ -337,9 +345,11 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var items = CreateMockHistoryItems();
|
var items = CreateMockHistoryItems();
|
||||||
var emptyHistory = CreateMockHistoryService(new());
|
var emptyHistory = CreateMockHistoryService(new());
|
||||||
var history = CreateMockHistoryService(items);
|
var history = CreateMockHistoryService(items);
|
||||||
|
var fuzzyMatcher = CreateMatcher();
|
||||||
|
|
||||||
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, emptyHistory)).ToList();
|
var q = fuzzyMatcher.PrecomputeQuery("C");
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
var unweightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, emptyHistory, fuzzyMatcher)).ToList();
|
||||||
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
|
Assert.AreEqual(unweightedScores.Count, weightedScores.Count, "Both score lists should have the same number of items");
|
||||||
for (var i = 0; i < unweightedScores.Count; i++)
|
for (var i = 0; i < unweightedScores.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -380,7 +390,10 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var items = CreateMockHistoryItems();
|
var items = CreateMockHistoryItems();
|
||||||
var emptyHistory = CreateMockHistoryService(new());
|
var emptyHistory = CreateMockHistoryService(new());
|
||||||
var history = CreateMockHistoryService(items);
|
var history = CreateMockHistoryService(items);
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
var fuzzyMatcher = CreateMatcher();
|
||||||
|
var q = fuzzyMatcher.PrecomputeQuery("te");
|
||||||
|
|
||||||
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||||
|
|
||||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||||
@@ -398,6 +411,8 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var items = CreateMockHistoryItems();
|
var items = CreateMockHistoryItems();
|
||||||
var emptyHistory = CreateMockHistoryService(new());
|
var emptyHistory = CreateMockHistoryService(new());
|
||||||
var history = CreateMockHistoryService(items);
|
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
|
// Add extra uses of VS Code to try and push it above Terminal
|
||||||
for (var i = 0; i < 10; i++)
|
for (var i = 0; i < 10; i++)
|
||||||
@@ -405,7 +420,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
history.AddHistoryItem(items[1].Id);
|
history.AddHistoryItem(items[1].Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("te", item, history)).ToList();
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||||
|
|
||||||
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
Assert.AreEqual(3, weightedMatches.Count, "Find Terminal, VsCode and Run commands");
|
||||||
@@ -423,6 +438,8 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
var items = CreateMockHistoryItems();
|
var items = CreateMockHistoryItems();
|
||||||
var emptyHistory = CreateMockHistoryService(new());
|
var emptyHistory = CreateMockHistoryService(new());
|
||||||
var history = CreateMockHistoryService(items);
|
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
|
// We're gonna run this test and keep adding more uses of VS Code till
|
||||||
// it breaks past Command Prompt
|
// it breaks past Command Prompt
|
||||||
@@ -431,7 +448,7 @@ public partial class RecentCommandsTests : CommandPaletteUnitTestBase
|
|||||||
{
|
{
|
||||||
history.AddHistoryItem(vsCodeId);
|
history.AddHistoryItem(vsCodeId);
|
||||||
|
|
||||||
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem("C", item, history)).ToList();
|
var weightedScores = items.Select(item => MainListPage.ScoreTopLevelItem(q, item, history, fuzzyMatcher)).ToList();
|
||||||
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
var weightedMatches = GetMatches(items, weightedScores).ToList();
|
||||||
Assert.AreEqual(4, weightedMatches.Count);
|
Assert.AreEqual(4, weightedMatches.Count);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user