merge main

This commit is contained in:
vanzue
2025-04-28 10:30:28 +08:00
76 changed files with 1348 additions and 705 deletions

View File

@@ -1279,6 +1279,7 @@ pstm
PStr
pstream
pstrm
pswd
PSYSTEM
psz
ptb
@@ -1425,6 +1426,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1980,4 +1982,12 @@ zoomit
ZOOMITX
ZXk
ZXNs
zzz
zzz
ACIE
AOklab
BCIE
BOklab
culori
Evercoder
LCh
CIELCh

View File

@@ -3,5 +3,5 @@
"notificationAliases": ["powertoys@microsoft.com"],
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
}

View File

@@ -141,6 +141,40 @@ namespace ManagedCommon
return lab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklab color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
{
var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
return oklab;
}
/// <summary>
/// Convert a given <see cref="Color"/> to a Oklch color
/// </summary>
/// <param name="color">The <see cref="Color"/> to convert</param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
{
var oklab = ConvertToOklabColor(color);
var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
return oklch;
}
public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
{
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
return (rLinear, gLinear, bLinear);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
@@ -156,10 +190,7 @@ namespace ManagedCommon
double g = color.G / 255d;
double b = color.B / 255d;
// inverse companding, gamma correction must be undone
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
(double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
return (
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
@@ -210,6 +241,63 @@ namespace ManagedCommon
return (l, a, b);
}
/// <summary>
/// Convert a linear RGB color <see cref="double"/> to an Oklab color.
/// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
/// and the implementation is based on https://bottosson.github.io/posts/oklab/
/// </summary>
/// <param name="r">Linear R value</param>
/// <param name="g">Linear G value</param>
/// <param name="b">Linear B value</param>
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
private static (double Lightness, double ChromaticityA, double ChromaticityB)
GetOklabColorFromLinearRGB(double r, double g, double b)
{
double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
double l_ = Math.Cbrt(l);
double m_ = Math.Cbrt(m);
double s_ = Math.Cbrt(s);
return (
(0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
(1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
(0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
);
}
/// <summary>
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
private static (double Lightness, double Chroma, double Hue)
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
{
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
}
/// <summary>
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
/// </summary>
/// <param name="lightness">The <see cref="lightness"/></param>
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
/// <returns>The lightness, chroma, and hue angle</returns>
private static (double Lightness, double Chroma, double Hue)
GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
{
// Lab to LCh transformation
double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
return (lightness, chroma, hue);
}
/// <summary>
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
/// </summary>
@@ -276,12 +364,17 @@ namespace ManagedCommon
{ "Br", 'p' }, // brightness percent
{ "In", 'p' }, // intensity percent
{ "Ll", 'p' }, // lightness (HSL) percent
{ "Lc", 'p' }, // lightness(CIELAB)percent
{ "Va", 'p' }, // value percent
{ "Wh", 'p' }, // whiteness percent
{ "Bn", 'p' }, // blackness percent
{ "Ca", 'p' }, // chromaticityA percent
{ "Cb", 'p' }, // chromaticityB percent
{ "Lc", 'p' }, // lightness (CIE) percent
{ "Ca", 'p' }, // chromaticityA (CIELAB) percent
{ "Cb", 'p' }, // chromaticityB (CIELAB) percent
{ "Lo", 'p' }, // lightness (Oklab/Oklch) percent
{ "Oa", 'p' }, // chromaticityA (Oklab) percent
{ "Ob", 'p' }, // chromaticityB (Oklab) percent
{ "Oc", 'p' }, // chroma (Oklch) percent
{ "Oh", 'p' }, // hue angle (Oklch) percent
{ "Xv", 'i' }, // X value int
{ "Yv", 'i' }, // Y value int
{ "Zv", 'i' }, // Z value int
@@ -424,6 +517,10 @@ namespace ManagedCommon
var (lightnessC, _, _) = ConvertToCIELABColor(color);
lightnessC = Math.Round(lightnessC, 2);
return lightnessC.ToString(CultureInfo.InvariantCulture);
case "Lo":
var (lightnessO, _, _) = ConvertToOklabColor(color);
lightnessO = Math.Round(lightnessO, 2);
return lightnessO.ToString(CultureInfo.InvariantCulture);
case "Wh":
var (_, whiteness, _) = ConvertToHWBColor(color);
whiteness = Math.Round(whiteness * 100);
@@ -440,6 +537,22 @@ namespace ManagedCommon
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
chromaticityB = Math.Round(chromaticityB, 2);
return chromaticityB.ToString(CultureInfo.InvariantCulture);
case "Oa":
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
case "Ob":
var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
case "Oc":
var (_, chromaOklch, _) = ConvertToOklchColor(color);
chromaOklch = Math.Round(chromaOklch, 2);
return chromaOklch.ToString(CultureInfo.InvariantCulture);
case "Oh":
var (_, _, hueOklch) = ConvertToOklchColor(color);
hueOklch = Math.Round(hueOklch, 2);
return hueOklch.ToString(CultureInfo.InvariantCulture);
case "Xv":
var (x, _, _) = ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
@@ -495,8 +608,10 @@ namespace ManagedCommon
case "HSI": return "hsi(%Hu, %Si%, %In%)";
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
case "NCol": return "%Hn, %Wh%, %Bn%";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
case "Decimal": return "%Dv";
case "HEX Int": return "0xFF%ReX%GrX%BlX";

View File

@@ -18,10 +18,19 @@ public static class OcrHelpers
{
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
var ocrLanguage = GetOCRLanguage();
cancellationToken.ThrowIfCancellationRequested();
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
OcrEngine ocrEngine;
if (ocrLanguage is not null)
{
ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
}
else
{
ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
}
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);

View File

@@ -2,7 +2,7 @@
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.prop')" />
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
@@ -53,7 +53,6 @@
<PreprocessorDefinitions>
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
%(PreprocessorDefinitions);
$(CommandPaletteBranding)
</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
IS_DEV_BRANDING;%(PreprocessorDefinitions)

View File

@@ -3,6 +3,7 @@
#include <interface/powertoy_module_interface.h>
#include <atomic>
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/SettingsAPI/settings_helpers.h>
@@ -10,10 +11,11 @@
#include <common/utils/resources.h>
#include <common/utils/package.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <common/interop/shared_constants.h>
#include <Psapi.h>
#include <TlHelp32.h>
#include <common/utils/winapi_error.h>
#include <thread>
HINSTANCE g_hInst_cmdPal = 0;
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
class CmdPal : public PowertoyModuleIface
{
private:
bool m_enabled = false;
std::wstring app_name;
//contains the non localized key of the powertoy
@@ -46,7 +46,10 @@ private:
HANDLE m_hTerminateEvent;
void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
// Track if this is the first call to enable
bool firstEnableCall = true;
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -54,6 +57,10 @@ private:
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
if (silentFail)
{
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
}
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
@@ -64,7 +71,11 @@ private:
{
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
return false;
}
m_launched.store(true);
return true;
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
@@ -122,6 +133,9 @@ private:
}
public:
static std::atomic<bool> m_enabled;
static std::atomic<bool> m_launched;
CmdPal()
{
app_name = L"CmdPal";
@@ -133,10 +147,7 @@ public:
~CmdPal()
{
if (m_enabled)
{
}
m_enabled = false;
CmdPal::m_enabled.store(false);
}
// Destroy the powertoy and free memory
@@ -203,15 +214,18 @@ public:
{
Logger::trace("CmdPal::enable()");
m_enabled = true;
CmdPal::m_enabled.store(true);
try
{
std::wstring packageName = L"Microsoft.CommandPalette";
std::wstring packageName = L"Microsoft.CommandPalette";
std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
#ifdef IS_DEV_BRANDING
packageName = L"Microsoft.CommandPalette.Dev";
packageName = L"Microsoft.CommandPalette.Dev";
launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
try
{
Logger::info(L"CmdPal not installed. Installing...");
@@ -238,28 +252,34 @@ public:
}
}
}
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
}
}
catch (std::exception& e)
if (!package::GetRegisteredPackage(packageName, false).has_value())
{
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
errorMessage += e.what();
Logger::error(errorMessage);
Logger::error("Cmdpal is not registered, quit..");
return;
}
try
if (!firstEnableCall)
{
#ifdef IS_DEV_BRANDING
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
#else
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
#endif
Logger::trace("Not first attempt, try to launch");
LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
}
catch (std::exception& e)
else
{
std::string errorMessage{ "Exception thrown while trying to launch CmdPal: " };
errorMessage += e.what();
Logger::error(errorMessage);
throw;
// If first time enable, do retry launch.
Logger::trace("First attempt, try to launch");
std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
launchThread.detach();
}
firstEnableCall = false;
}
virtual void disable()
@@ -267,7 +287,44 @@ public:
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
m_enabled = false;
CmdPal::m_enabled.store(false);
}
static void RetryLaunch(std::wstring path)
{
const int base_delay_milliseconds = 1000;
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
int retry = 0;
do
{
auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
if (launch_result)
{
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
return;
}
else
{
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
}
// When we got max retry, we don't need to wait for the next retry.
if (retry < max_retry)
{
int delay = base_delay_milliseconds * (1 << (retry));
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
}
++retry;
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
if (!m_enabled.load() || m_launched.load())
{
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
}
else
{
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
}
}
virtual bool on_hotkey(size_t) override
@@ -282,11 +339,14 @@ public:
virtual bool is_enabled() override
{
return m_enabled;
return CmdPal::m_enabled.load();
}
};
std::atomic<bool> CmdPal::m_enabled{ false };
std::atomic<bool> CmdPal::m_launched{ false };
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Contracts;
public interface IFileService
{
T Read<T>(string folderPath, string fileName);
void Save<T>(string folderPath, string fileName, T content);
void Delete(string folderPath, string fileName);
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Contracts;
public interface ILocalSettingsService
{
Task<bool> HasSettingAsync(string key);
Task<T?> ReadSettingAsync<T>(string key);
Task SaveSettingAsync<T>(string key, T value);
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.UI.Xaml;
namespace Microsoft.CmdPal.Common.Extensions;
/// <summary>
/// Extension class implementing extension methods for <see cref="Application"/>.
/// </summary>
public static class ApplicationExtensions
{
/// <summary>
/// Get registered services at the application level from anywhere in the
/// application.
///
/// Note:
/// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
/// "Application is a singleton that implements the static Current property
/// to provide shared access to the Application instance for the current
/// application. The singleton pattern ensures that state managed by
/// Application, including shared resources and properties, is available
/// from a single, shared location."
///
/// Example of usage:
/// <code>
/// Application.Current.GetService<T>()
/// </code>
/// </summary>
/// <typeparam name="T">Service type.</typeparam>
/// <param name="application">Current application.</param>
/// <returns>Service reference.</returns>
public static T GetService<T>(this Application application)
where T : class
{
return (application as IApp)!.GetService<T>();
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.CmdPal.Common.Extensions;
public static class IHostExtensions
{
/// <summary>
/// <inheritdoc cref="ActivatorUtilities.CreateInstance(IServiceProvider, Type, object[])"/>
/// </summary>
public static T CreateInstance<T>(this IHost host, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(host.Services, parameters);
}
/// <summary>
/// Gets the service object for the specified type, or throws an exception
/// if type was not registered.
/// </summary>
/// <typeparam name="T">Service type</typeparam>
/// <param name="host">Host object</param>
/// <returns>Service object</returns>
/// <exception cref="ArgumentException">Throw an exception if the specified
/// type is not registered</exception>
public static T GetService<T>(this IHost host)
where T : class
{
if (host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Helpers;
public static class Json
{
public static async Task<T> ToObjectAsync<T>(string value)
{
if (typeof(T) == typeof(bool))
{
return (T)(object)bool.Parse(value);
}
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
return (await JsonSerializer.DeserializeAsync<T>(stream))!;
}
public static async Task<string> StringifyAsync<T>(T value)
{
if (typeof(T) == typeof(bool))
{
return value!.ToString()!.ToLowerInvariant();
}
await using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, value);
return Encoding.UTF8.GetString(stream.ToArray());
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Common.Helpers;
public static class NativeEventWaiter
public static partial class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{

View File

@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.Common.Helpers;
public static class RuntimeHelper
public static partial class RuntimeHelper
{
public static bool IsMSIX
{

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.Common.Messages;
public record HideWindowMessage()
public partial record HideWindowMessage()
{
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
<Nullable>enable</Nullable>

View File

@@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Models;
public class LocalSettingsOptions
{
public string? ApplicationDataFolder
{
get; set;
}
public string? LocalSettingsFile
{
get; set;
}
}

View File

@@ -1,16 +1,7 @@
EnableWindow
CoCreateInstance
FileOpenDialog
FileSaveDialog
IFileOpenDialog
IFileSaveDialog
SHCreateItemFromParsingName
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
SHLoadIndirectString
StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
GetDpiForWindow

View File

@@ -1,48 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.IO;
using System.Text;
using System.Text.Json;
using Microsoft.CmdPal.Common.Contracts;
namespace Microsoft.CmdPal.Common.Services;
public class FileService : IFileService
{
private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
#pragma warning disable CS8603 // Possible null reference return.
public T Read<T>(string folderPath, string fileName)
{
var path = Path.Combine(folderPath, fileName);
if (File.Exists(path))
{
using var fileStream = File.OpenText(path);
return JsonSerializer.Deserialize<T>(fileStream.BaseStream);
}
return default;
}
#pragma warning restore CS8603 // Possible null reference return.
public void Save<T>(string folderPath, string fileName, T content)
{
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
var fileContent = JsonSerializer.Serialize(content);
File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
}
public void Delete(string folderPath, string fileName)
{
if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
{
File.Delete(Path.Combine(folderPath, fileName));
}
}
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Services;
/// <summary>
/// Interface for the current application singleton object exposing the API
/// that can be accessed from anywhere in the application.
/// </summary>
public interface IApp
{
/// <summary>
/// Gets services registered at the application level.
/// </summary>
public T GetService<T>()
where T : class;
}

View File

@@ -1,120 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Contracts;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Models;
using Microsoft.Extensions.Options;
using Windows.Storage;
namespace Microsoft.CmdPal.Common.Services;
public class LocalSettingsService : ILocalSettingsService
{
// TODO! for now, we're hardcoding the path as effectively:
// %localappdata%\CmdPal\LocalSettings.json
private const string DefaultApplicationDataFolder = "CmdPal";
private const string DefaultLocalSettingsFile = "LocalSettings.json";
private readonly IFileService _fileService;
private readonly LocalSettingsOptions _options;
private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
private readonly string _applicationDataFolder;
private readonly string _localSettingsFile;
private readonly bool _isMsix;
private Dictionary<string, object> _settings;
private bool _isInitialized;
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
{
_isMsix = false; // RuntimeHelper.IsMSIX;
_fileService = fileService;
_options = options.Value;
_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
_localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
_settings = new Dictionary<string, object>();
}
private async Task InitializeAsync()
{
if (!_isInitialized)
{
_settings = await Task.Run(() => _fileService.Read<Dictionary<string, object>>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary<string, object>();
_isInitialized = true;
}
}
public async Task<bool> HasSettingAsync(string key)
{
if (_isMsix)
{
return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
}
else
{
await InitializeAsync();
if (_settings != null)
{
return _settings.ContainsKey(key);
}
}
return false;
}
public async Task<T?> ReadSettingAsync<T>(string key)
{
if (_isMsix)
{
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
}
else
{
await InitializeAsync();
if (_settings != null && _settings.TryGetValue(key, out var obj))
{
var s = obj.ToString();
if (s != null)
{
return await Json.ToObjectAsync<T>(s);
}
}
}
return default;
}
public async Task SaveSettingAsync<T>(string key, T value)
{
if (_isMsix)
{
ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
}
else
{
await InitializeAsync();
_settings[key] = await Json.StringifyAsync(value!);
await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
}
}
}

View File

@@ -49,7 +49,7 @@ public partial class AppStateModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -73,7 +73,7 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
return Path.Combine(directory, "state.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
AllowTrailingCommas = true,
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
ReadCommentHandling = JsonCommentHandling.Skip,
};
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// AllowTrailingCommas = true,
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
// ReadCommentHandling = JsonCommentHandling.Skip,
// };
}

View File

@@ -4,18 +4,14 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>,
IRecipient<UpdateItemKeybindingsMessage>
IRecipient<UpdateCommandBarMessage>
{
public ICommandBarContext? SelectedItem
{
@@ -53,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
@@ -111,7 +104,10 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
ContextCommands = [.. SelectedItem.AllCommands.Where(c => c.ShouldBeVisible)];
ContextMenuStack.Clear();
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
OnPropertyChanged(nameof(ContextMenu));
}
else
{
@@ -125,43 +121,80 @@ public partial class CommandBarViewModel : ObservableObject,
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
[RelayCommand]
private void InvokeItem(CommandContextItemViewModel item) =>
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
// [RelayCommand]
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
if (PrimaryCommand != null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
}
PerformCommand(SecondaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
if (SecondaryCommand != null)
PerformCommand(SecondaryCommand);
}
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
}
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
{
if (command == null)
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
return ContextKeybindingResult.Unhandled;
}
if (command.HasMoreCommands)
{
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
return ContextKeybindingResult.KeepOpen;
}
else
{
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
return ContextKeybindingResult.Hide;
}
}
public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
public bool CanPopContextStack()
{
if (_contextKeybindings != null)
return ContextMenuStack.Count > 1;
}
public void PopContextStack()
{
if (ContextMenuStack.Count > 1)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
return true;
}
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
return false;
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
public void ClearContextStack()
{
while (ContextMenuStack.Count > 1)
{
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
OnPropertyChanging(nameof(ContextMenu));
OnPropertyChanged(nameof(ContextMenu));
}
}
public enum ContextKeybindingResult
{
Unhandled,
Hide,
KeepOpen,
}

View File

@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
contextItem.InitializeProperties();
contextItem.SlowInitializeProperties();
});
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
if (!string.IsNullOrEmpty(model.Command.Name))
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,
Command = Command,
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
};
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
// Only set the icon on the context item for us if our command didn't
// have its own icon
if (!Command.HasIcon)
{
_defaultCommandContextItem._listItemIcon = _listItemIcon;
}
}
Initialized |= InitializedState.SelectionInitialized;
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
internal Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
[Flags]

View File

@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
{
public CreatedExtensionForm(string name, string displayName, string path)
{
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
TemplateJson = CardTemplate;
DataJson = $$"""
{
"name": {{JsonSerializer.Serialize(name)}},
"directory": {{JsonSerializer.Serialize(path)}},
"displayName": {{JsonSerializer.Serialize(displayName)}}
"name": {{serializeString(name)}},
"directory": {{serializeString(path)}},
"displayName": {{serializeString(displayName)}}
}
""";
_name = name;
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
{
return CommandResult.KeepOpen();
}
string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
return verb switch
{
"sln" => OpenSolution(),
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenSolution()
{
string[] parts = [_path, _name, $"{_name}.sln"];
string pathToSolution = Path.Combine(parts);
var pathToSolution = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToSolution);
return CommandResult.Hide();
}
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenDirectory()
{
string[] parts = [_path, _name];
string pathToDir = Path.Combine(parts);
var pathToDir = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToDir);
return CommandResult.Hide();
}

View File

@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
Directory.Delete(tempDir, true);
}
private string FormatJsonString(string str)
{
private string FormatJsonString(string str) =>
// Escape the string for JSON
return JsonSerializer.Serialize(str);
}
JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
}

View File

@@ -51,15 +51,16 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
// If we fail to parse the card JSON, then display _our own card_
// with the exception
AdaptiveCardTemplate template = new(ErrorCardJson);
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
"error_message": {{JsonSerializer.Serialize(e.Message)}},
"error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
"inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
"template_json": {{JsonSerializer.Serialize(TemplateJson)}},
"data_json": {{JsonSerializer.Serialize(DataJson)}}
"error_message": {{serializeString(e.Message)}},
"error_stack": {{serializeString(e.StackTrace)}},
"inner_exception": {{serializeString(e.InnerException?.Message)}},
"template_json": {{serializeString(TemplateJson)}},
"data_json": {{serializeString(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContextMenuStackViewModel : ObservableObject
{
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
private readonly IContextMenuContext _context;
private string _lastSearchText = string.Empty;
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public ContextMenuStackViewModel(IContextMenuContext context)
{
_context = context;
FilteredItems = [.. context.AllCommands];
}
public void SetSearchText(string searchText)
{
if (searchText == _lastSearchText)
{
return;
}
_lastSearchText = searchText;
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
if (string.IsNullOrEmpty(searchText))
{
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
return;
}
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
}
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
{
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
{
return 1;
}
if (string.IsNullOrEmpty(item.Title))
{
return 0;
}
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
}
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
var keybindings = _context.Keybindings();
if (keybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (keybindings.TryGetValue(pressedKeyChord, out var item))
{
return item;
}
}
return null;
}
}

View File

@@ -344,8 +344,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));

View File

@@ -2,8 +2,11 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record UpdateItemKeybindingsMessage(Dictionary<KeyChord, CommandContextItemViewModel>? Keys);
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
{
public bool Handled { get; set; }
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : INotifyPropertyChanged
public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
public bool HasMoreCommands { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
// Represents everything the command bar needs to know about to show command
// buttons at the bottom.
//
// This is implemented by both ListItemViewModel and ContentPageViewModel,
// the two things with sub-commands.
public interface ICommandBarContext : IContextMenuContext
{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
public List<CommandContextItemViewModel> AllCommands { get; }
}

View File

@@ -1,5 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -67,4 +69,15 @@
</Content>
</ItemGroup>
<!-- Just mark it as AOT compatible. Do not publish with AOT now. We need fully test before we really publish it as AOT enabled-->
<!--<PropertyGroup>
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<PublishTrimmed>true</PublishTrimmed>
<PublishSingleFile>true</PublishSingleFile>
--><!-- <DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> --><!--
<PublishAot>true</PublishAot>
<EnableMsixTooling>true</EnableMsixTooling>
</PropertyGroup>-->
</Project>

View File

@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionService : IExtensionService, IDisposable
public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;

View File

@@ -12,6 +12,7 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
if (hr.Value == -2147024893)
unsafe
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
if (hr.Value == -2147024893)
{
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
}
extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
// extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
extensionPtr = (nint)extensionObj;
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
if (hr < 0)
{
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
finally
{

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}

View File

@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
private Task<bool> InitializeAsync()
internal Task<bool> InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
case nameof(Icon):
this.Icon = new(model.Icon);
break;
default:
updateProperty = false;
break;
}
UpdateProperty(propertyName);
// GH #38829: If we always UpdateProperty here, then there's a possible
// race condition, where we raise the PropertyChanged(SearchText)
// before the subclass actually retrieves the new SearchText from the
// model. In that race situation, if the UI thread handles the
// PropertyChanged before ListViewModel fetches the SearchText, it'll
// think that the old search text is the _new_ value.
if (updateProperty)
{
UpdateProperty(propertyName);
}
}
public new void ShowException(Exception ex, string? extensionHint = null)

View File

@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
private List<HistoryItem> History { get; set; } = [];
internal List<HistoryItem> History { get; set; } = [];
public RecentCommandsManager()
{

View File

@@ -93,7 +93,7 @@ public partial class SettingsModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, _deserializerOptions);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -117,7 +117,7 @@ public partial class SettingsModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -133,7 +133,7 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
var serialized = savedSettings.ToJsonString(_serializerOptions);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -166,19 +166,34 @@ public partial class SettingsModel : ObservableObject
return Path.Combine(directory, "settings.json");
}
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
// private static readonly JsonSerializerOptions _serializerOptions = new()
// {
// WriteIndented = true,
// Converters = { new JsonStringEnumConverter() },
// };
// private static readonly JsonSerializerOptions _deserializerOptions = new()
// {
// PropertyNameCaseInsensitive = true,
// IncludeFields = true,
// Converters = { new JsonStringEnumConverter() },
// AllowTrailingCommas = true,
// };
}
private static readonly JsonSerializerOptions _deserializerOptions = new()
{
PropertyNameCaseInsensitive = true,
IncludeFields = true,
Converters = { new JsonStringEnumConverter() },
AllowTrailingCommas = true,
};
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(AppStateModel))]
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal sealed partial class JsonSerializationContext : JsonSerializerContext
{
}
public enum MonitorBehavior

View File

@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
() =>
async () =>
{
var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// bool f = await viewModel.InitializeCommand.ExecutionTask.;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault<bool?>()!;
var result = await viewModel.InitializeAsync();
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;

View File

@@ -225,27 +225,42 @@
ToolTipService.ToolTip="Ctrl+K"
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedRight">
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<Flyout
Closing="Flyout_Closing"
Opened="Flyout_Opened"
Placement="TopEdgeAlignedRight">
<StackPanel>
<ListView
x:Name="CommandsDropdown"
MinWidth="248"
Margin="-16,-12,-16,-12"
IsItemClickEnabled="True"
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="Single">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,7,12,7" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemContainerTransitions>
<TransitionCollection />
</ListView.ItemContainerTransitions>
</ListView>
<TextBox
x:Name="ContextFilterBox"
x:Uid="ContextFilterBox"
Margin="-12,12,-12,-12"
KeyDown="ContextFilterBox_KeyDown"
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
TextChanged="ContextFilterBox_TextChanged" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>

View File

@@ -18,9 +18,10 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient<OpenContextMenuMessage>,
IRecipient<TryCommandKeybindingMessage>,
ICurrentPageAware
{
public CommandBarViewModel ViewModel { get; set; } = new();
public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
@@ -38,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -52,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
CommandsDropdown.SelectedIndex = 0;
CommandsDropdown.Focus(FocusState.Programmatic);
UpdateUiForStackChange();
}
public void Receive(TryCommandKeybindingMessage msg)
{
if (!ViewModel.ShouldShowContextMenu)
{
return;
}
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
if (result == ContextKeybindingResult.Hide)
{
msg.Handled = true;
}
else if (result == ContextKeybindingResult.KeepOpen)
{
if (!MoreCommandsButton.Flyout.IsOpen)
{
var options = new FlyoutShowOptions
{
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
}
UpdateUiForStackChange();
msg.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
msg.Handled = false;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
@@ -88,8 +125,14 @@ public sealed partial class CommandBar : UserControl,
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
ViewModel?.InvokeItemCommand.Execute(item);
MoreCommandsButton.Flyout.Hide();
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
}
else
{
UpdateUiForStackChange();
}
}
}
@@ -106,9 +149,136 @@ public sealed partial class CommandBar : UserControl,
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
if (result == ContextKeybindingResult.Hide)
{
e.Handled = true;
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else if (result == ContextKeybindingResult.KeepOpen)
{
e.Handled = true;
}
else if (result == ContextKeybindingResult.Unhandled)
{
e.Handled = false;
}
}
private void Flyout_Opened(object sender, object e)
{
UpdateUiForStackChange();
}
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
{
ViewModel?.ClearContextStack();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var prop = e.PropertyName;
if (prop == nameof(ViewModel.ContextMenu))
{
UpdateUiForStackChange();
}
}
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
if (CommandsDropdown.SelectedIndex == -1)
{
CommandsDropdown.SelectedIndex = 0;
}
}
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (e.Key == VirtualKey.Enter)
{
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
{
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
else
{
UpdateUiForStackChange();
}
e.Handled = true;
}
}
else if (e.Key == VirtualKey.Escape ||
(e.Key == VirtualKey.Left && altPressed))
{
if (ViewModel.CanPopContextStack())
{
ViewModel.PopContextStack();
UpdateUiForStackChange();
}
else
{
MoreCommandsButton.Flyout.Hide();
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
e.Handled = true;
}
CommandsDropdown_KeyDown(sender, e);
}
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Up)
{
// navigate previous
if (CommandsDropdown.SelectedIndex > 0)
{
CommandsDropdown.SelectedIndex--;
}
else
{
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
}
e.Handled = true;
}
else if (e.Key == VirtualKey.Down)
{
// navigate next
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
{
CommandsDropdown.SelectedIndex++;
}
else
{
CommandsDropdown.SelectedIndex = 0;
}
e.Handled = true;
}
}
private void UpdateUiForStackChange()
{
ContextFilterBox.Text = string.Empty;
ViewModel.ContextMenu?.SetSearchText(string.Empty);
CommandsDropdown.SelectedIndex = 0;
ContextFilterBox.Focus(FocusState.Programmatic);
}
}

View File

@@ -8,8 +8,6 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient<GoHomeMessage>,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateItemKeybindingsMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void ClearSearch()
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
}
if (_keyBindings != null)
if (!e.Handled)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
e.Handled = true;
}
// The CommandBar is responsible for handling all the item keybindings,
// since the bound context item may need to then show another
// context menu
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
WeakReferenceMessenger.Default.Send(msg);
e.Handled = msg.Handled;
}
}
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(UpdateItemKeybindingsMessage message)
{
_keyBindings = message.Keys;
}
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}

View File

@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.

View File

@@ -394,6 +394,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
<value>Search commands...</value>
</data>
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
<value>Show system tray icon</value>
</data>

View File

@@ -34,7 +34,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
"value": {{JsonSerializer.Serialize(name)}},
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
},
@@ -42,7 +42,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"type": "Input.Text",
"style": "text",
"id": "bookmark",
"value": {{JsonSerializer.Serialize(url)}},
"value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.Bookmarks;
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(BookmarkData))]
[JsonSerializable(typeof(Bookmarks))]
[JsonSerializable(typeof(List<BookmarkData>), TypeInfoPropertyName = "BookmarkList")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class BookmarkSerializationContext : JsonSerializerContext
{
}

View File

@@ -28,7 +28,7 @@ public sealed class Bookmarks
if (!string.IsNullOrEmpty(jsonStringReading))
{
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, _jsonOptions) ?? new Bookmarks();
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
}
}
@@ -37,7 +37,7 @@ public sealed class Bookmarks
public static void WriteToFile(string path, Bookmarks data)
{
var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks</RootNamespace>
<Nullable>enable</Nullable>
@@ -16,7 +17,7 @@
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
@@ -24,7 +25,7 @@
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Update="Assets\Bookmark.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -39,5 +40,5 @@
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@ internal static class NativeMethods
internal INPUTTYPE type;
internal InputUnion data;
internal static int Size => Marshal.SizeOf(typeof(INPUT));
internal static int Size => Marshal.SizeOf<INPUT>();
}
[StructLayout(LayoutKind.Explicit)]

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>

View File

@@ -311,14 +311,14 @@ internal sealed class NetworkConnectionProperties
{
switch (property)
{
case string:
return string.IsNullOrWhiteSpace(property) ? string.Empty : $"\n\n{title}{property}";
case string str:
return string.IsNullOrWhiteSpace(str) ? string.Empty : $"\n\n{title}{str}";
case List<string> listString:
return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listString)}";
case List<IPAddress> listIP:
return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listIP)}";
case IPAddressCollection collectionIP:
return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", collectionIP)}";
case null:
return string.Empty;
default:

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.CmdPal.Ext.System</RootNamespace>

View File

@@ -13,5 +13,5 @@ public class HistoryItem(string searchString, DateTime timestamp)
public DateTime Timestamp { get; private set; } = timestamp;
public string ToJson() => JsonSerializer.Serialize(this);
public string ToJson() => JsonSerializer.Serialize(this, WebSearchJsonSerializationContext.Default.HistoryItem);
}

View File

@@ -80,7 +80,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent) ?? [];
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
@@ -101,7 +101,7 @@ public class SettingsManager : JsonSettingsManager
}
// Serialize the updated list back to JSON and save it
var historyJson = JsonSerializer.Serialize(historyItems);
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
}
catch (Exception ex)
@@ -121,7 +121,7 @@ public class SettingsManager : JsonSettingsManager
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent) ?? [];
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List<ListItem>();
@@ -198,7 +198,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent) ?? [];
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > maxHistoryItems)
@@ -207,7 +207,7 @@ public class SettingsManager : JsonSettingsManager
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems);
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
namespace Microsoft.CmdPal.Ext.WebSearch;
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(List<HistoryItem>))]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class WebSearchJsonSerializationContext : JsonSerializerContext
{
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.WebSearch</RootNamespace>
<Nullable>enable</Nullable>

View File

@@ -4,10 +4,8 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers;
@@ -21,6 +19,8 @@ internal static class JsonSettingsListHelper
/// </summary>
private const string _settingsFile = "WindowsSettings.json";
private const string _extTypeNamespace = "Microsoft.CmdPal.Ext.WindowsSettings";
private static readonly JsonSerializerOptions _serializerOptions = new()
{
};
@@ -32,7 +32,6 @@ internal static class JsonSettingsListHelper
internal static Classes.WindowsSettings ReadAllPossibleSettings()
{
var assembly = Assembly.GetExecutingAssembly();
var type = assembly.GetTypes().FirstOrDefault(x => x.Name == nameof(WindowsSettingsCommandsProvider));
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Classes.WindowsSettings? settings = null;
@@ -40,7 +39,7 @@ internal static class JsonSettingsListHelper
try
{
var resourceName = $"{type?.Namespace}.{_settingsFile}";
var resourceName = $"{_extTypeNamespace}.{_settingsFile}";
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
@@ -48,12 +47,13 @@ internal static class JsonSettingsListHelper
}
var options = _serializerOptions;
options.Converters.Add(new JsonStringEnumConverter());
// Why we need it? I don't see any enum usage in WindowsSettings
// options.Converters.Add(new JsonStringEnumConverter());
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
settings = JsonSerializer.Deserialize<Classes.WindowsSettings>(text, options);
settings = JsonSerializer.Deserialize(text, WindowsSettingsJsonSerializationContext.Default.WindowsSettings);
}
#pragma warning disable CS0168
catch (Exception exception)

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Ext.WindowsSettings;
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Classes.WindowsSettings))]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class WindowsSettingsJsonSerializationContext : JsonSerializerContext
{
}

View File

@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.WindowsSettings</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>

View File

@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace SamplePagesExtension;
@@ -76,7 +77,136 @@ public partial class EvilSamplesPage : ListPage
{
Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
},
}
},
// More edge cases than truly evil
new ListItem(
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "anonymous command test",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem("nested...")
{
Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
{
Title = "Nested A",
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
},
new CommandContextItem(
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
{
Title = "Nested B...",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested C invoked") { Name = "Do it" })
{
Title = "You get it",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
}
],
},
],
}
],
},
new ListItem(
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "noop command test",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(new NoOpCommand())
{
Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
{
Title = "Nested A",
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
},
new CommandContextItem(
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
{
Title = "Nested B...",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested C invoked") { Name = "Do it" })
{
Title = "You get it",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
}
],
},
],
}
],
},
new ListItem(
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "noop secondary command test",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(new NoOpCommand())
{
Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
{
Title = "Nested A",
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
},
new CommandContextItem(
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
{
Title = "Nested B...",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested C invoked") { Name = "Do it" })
{
Title = "You get it",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
}
],
},
],
}
],
},
];
public EvilSamplesPage()

View File

@@ -69,62 +69,47 @@ internal sealed partial class SampleListPage : ListPage
},
new ListItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Primary command invoked",
State = MessageState.Info,
});
t.Show();
})
{
Result = CommandResult.KeepOpen(),
Icon = new IconInfo("\uE712"),
})
new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "You can add context menu items too. Press Ctrl+k",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"),
Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Secondary command invoked",
State = MessageState.Warning,
});
t.Show();
})
{
Name = "Secondary command",
Icon = new IconInfo("\uF147"), // Dial 2
Result = CommandResult.KeepOpen(),
})
new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Third command invoked",
State = MessageState.Error,
});
t.Show();
})
{
Name = "Do it",
Icon = new IconInfo("\uF148"), // dial 3
Result = CommandResult.KeepOpen(),
})
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
{
Title = "A third command too",
Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
{
Title = "Nested A",
RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
},
new CommandContextItem(
new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
{
Title = "Nested B...",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
MoreCommands = [
new CommandContextItem(
new ToastCommand("Nested C invoked") { Name = "Do it" })
{
Title = "You get it",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
}
],
},
],
}
],
},
@@ -183,7 +168,6 @@ internal sealed partial class SampleListPage : ListPage
{
Title = "Get the name of the Foreground window",
},
];
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class ToastCommand(string message, MessageState state = MessageState.Info) : InvokableCommand
{
public override ICommandResult Invoke()
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = message,
State = state,
});
t.Show();
return CommandResult.KeepOpen();
}
}

View File

@@ -16,7 +16,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
[JsonSerializable(typeof(List<ChoiceSetSetting>))]
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal partial class JsonSerializationContext : JsonSerializerContext
{
}

View File

@@ -61,4 +61,9 @@ public partial class ListItem : CommandItem, IListItem
: base(command)
{
}
public ListItem()
: base()
{
}
}

View File

@@ -243,6 +243,40 @@ namespace ColorPicker.Helpers
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a Oklab color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the Oklab color presentation</param>
/// <returns>A <see cref="string"/> representation of a Oklab color</returns>
private static string ColorToOklab(Color color)
{
var (lightness, chromaticityA, chromaticityB) = ColorFormatHelper.ConvertToOklabColor(color);
lightness = Math.Round(lightness, 2);
chromaticityA = Math.Round(chromaticityA, 2);
chromaticityB = Math.Round(chromaticityB, 2);
return $"oklab({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a CIE LCh color
/// </summary>
/// <param name="color">The <see cref="Color"/> for the CIE LCh color presentation</param>
/// <returns>A <see cref="string"/> representation of a CIE LCh color</returns>
private static string ColorToOklch(Color color)
{
var (lightness, chroma, hue) = ColorFormatHelper.ConvertToOklchColor(color);
lightness = Math.Round(lightness, 2);
chroma = Math.Round(chroma, 2);
hue = Math.Round(hue, 2);
return $"oklch({lightness.ToString(CultureInfo.InvariantCulture)}" +
$", {chroma.ToString(CultureInfo.InvariantCulture)}" +
$", {hue.ToString(CultureInfo.InvariantCulture)})";
}
/// <summary>
/// Returns a <see cref="string"/> representation of a CIE XYZ color
/// </summary>

View File

@@ -301,6 +301,12 @@ namespace ColorPicker.ViewModels
FormatName = ColorRepresentationType.NCol.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIEXYZ.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
@@ -310,8 +316,14 @@ namespace ColorPicker.ViewModels
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.CIEXYZ.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
FormatName = ColorRepresentationType.Oklab.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklab.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()
{
FormatName = ColorRepresentationType.Oklch.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklch.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()

View File

@@ -364,9 +364,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
[DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("BFBF00", 75.04, -17.35, 76.03)] // yellow
[DataRow("008000", 46.23, -51.70, 49.90)] // green
[DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
[DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("0048BA", 34.35, 27.94, -64.80)] // absolute zero
[DataRow("B0BF1A", 73.91, -23.39, 71.15)] // acid green
[DataRow("D0FF14", 93.87, -40.20, 88.97)] // arctic lime
@@ -401,13 +398,121 @@ namespace Microsoft.ColorPicker.UnitTests
var result = ColorFormatHelper.ConvertToCIELABColor(color);
// lightness[0..100]
Assert.AreEqual(Math.Round(result.Lightness, 2), lightness);
Assert.AreEqual(lightness, Math.Round(result.Lightness, 2));
// chromaticityA[-128..127]
Assert.AreEqual(Math.Round(result.ChromaticityA, 2), chromaticityA);
Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
// chromaticityB[-128..127]
Assert.AreEqual(Math.Round(result.ChromaticityB, 2), chromaticityB);
Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
}
// Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
[TestMethod]
[DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
[DataRow("808080", 0.6, 0.00, 0.00)] // gray
[DataRow("000000", 0.00, 0.00, 0.00)] // black
[DataRow("FF0000", 0.628, 0.22, 0.13)] // red
[DataRow("008000", 0.52, -0.14, 0.11)] // green
[DataRow("80FFFF", 0.928, -0.11, -0.03)] // cyan
[DataRow("8080FF", 0.661, 0.03, -0.18)] // blue
[DataRow("BF40BF", 0.598, 0.18, -0.11)] // magenta
[DataRow("BFBF00", 0.779, -0.06, 0.16)] // yellow
[DataRow("0048BA", 0.444, -0.03, -0.19)] // absolute zero
[DataRow("B0BF1A", 0.767, -0.07, 0.15)] // acid green
[DataRow("D0FF14", 0.934, -0.12, 0.19)] // arctic lime
[DataRow("1B4D3E", 0.382, -0.06, 0.01)] // brunswick green
[DataRow("FFEF00", 0.935, -0.05, 0.19)] // canary yellow
[DataRow("FFA600", 0.794, 0.06, 0.16)] // cheese
[DataRow("1A2421", 0.25, -0.02, 0)] // dark jungle green
[DataRow("003399", 0.371, -0.02, -0.17)] // dark powder blue
[DataRow("D70A53", 0.563, 0.22, 0.04)] // debian red
[DataRow("80FFD5", 0.916, -0.13, 0.02)] // fathom secret green
[DataRow("EFDFBB", 0.907, 0, 0.05)] // dutch white
[DataRow("5218FA", 0.489, 0.05, -0.28)] // han purple
[DataRow("FF496C", 0.675, 0.21, 0.05)] // infra red
[DataRow("545AA7", 0.5, 0.02, -0.12)] // liberty
[DataRow("E6A8D7", 0.804, 0.09, -0.04)] // light orchid
[DataRow("ADDFAD", 0.856, -0.07, 0.05)] // light moss green
[DataRow("E3F988", 0.942, -0.07, 0.12)] // mindaro
public void ColorRGBtoOklabTest(string hexValue, double lightness, double chromaticityA, double chromaticityB)
{
if (string.IsNullOrWhiteSpace(hexValue))
{
Assert.IsNotNull(hexValue);
}
Assert.IsTrue(hexValue.Length >= 6);
var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var color = Color.FromArgb(255, red, green, blue);
var result = ColorFormatHelper.ConvertToOklabColor(color);
// lightness[0..1]
Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
// chromaticityA[-0.5..0.5]
Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
// chromaticityB[-0.5..0.5]
Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
}
// Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
[TestMethod]
[DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
[DataRow("808080", 0.6, 0.00, 0.00)] // gray
[DataRow("000000", 0.00, 0.00, 0.00)] // black
[DataRow("FF0000", 0.628, 0.258, 29.23)] // red
[DataRow("008000", 0.52, 0.177, 142.5)] // green
[DataRow("80FFFF", 0.928, 0.113, 195.38)] // cyan
[DataRow("8080FF", 0.661, 0.184, 280.13)] // blue
[DataRow("BF40BF", 0.598, 0.216, 327.86)] // magenta
[DataRow("BFBF00", 0.779, 0.17, 109.77)] // yellow
[DataRow("0048BA", 0.444, 0.19, 260.86)] // absolute zero
[DataRow("B0BF1A", 0.767, 0.169, 115.4)] // acid green
[DataRow("D0FF14", 0.934, 0.224, 122.28)] // arctic lime
[DataRow("1B4D3E", 0.382, 0.06, 170.28)] // brunswick green
[DataRow("FFEF00", 0.935, 0.198, 104.67)] // canary yellow
[DataRow("FFA600", 0.794, 0.171, 71.19)] // cheese
[DataRow("1A2421", 0.25, 0.015, 174.74)] // dark jungle green
[DataRow("003399", 0.371, 0.173, 262.12)] // dark powder blue
[DataRow("D70A53", 0.563, 0.222, 11.5)] // debian red
[DataRow("80FFD5", 0.916, 0.129, 169.38)] // fathom secret green
[DataRow("EFDFBB", 0.907, 0.05, 86.89)] // dutch white
[DataRow("5218FA", 0.489, 0.286, 279.13)] // han purple
[DataRow("FF496C", 0.675, 0.217, 14.37)] // infra red
[DataRow("545AA7", 0.5, 0.121, 277.7)] // liberty
[DataRow("E6A8D7", 0.804, 0.095, 335.4)] // light orchid
[DataRow("ADDFAD", 0.856, 0.086, 144.78)] // light moss green
[DataRow("E3F988", 0.942, 0.141, 118.24)] // mindaro
public void ColorRGBtoOklchTest(string hexValue, double lightness, double chroma, double hue)
{
if (string.IsNullOrWhiteSpace(hexValue))
{
Assert.IsNotNull(hexValue);
}
Assert.IsTrue(hexValue.Length >= 6);
var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
var color = Color.FromArgb(255, red, green, blue);
var result = ColorFormatHelper.ConvertToOklchColor(color);
// lightness[0..1]
Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
// chroma[0..0.5]
Assert.AreEqual(chroma, Math.Round(result.Chroma, 3));
// hue[0°..360°]
Assert.AreEqual(hue, Math.Round(result.Hue, 2));
}
// The following results are computed using LittleCMS2, an open-source color management engine,
@@ -428,9 +533,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
[DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("BFBF00", 40.1154, 48.3384, 7.2171)] // yellow
[DataRow("008000", 7.7188, 15.4377, 2.5729)] // green
[DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
[DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("0048BA", 11.1792, 8.1793, 47.4455)] // absolute zero
[DataRow("B0BF1A", 36.7205, 46.5663, 8.0311)] // acid green
[DataRow("D0FF14", 61.8965, 84.9797, 13.8037)] // arctic lime

View File

@@ -23,8 +23,10 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("HSV", "hsv(0, 0%, 0%)")]
[DataRow("HWB", "hwb(0, 0%, 100%)")]
[DataRow("RGB", "rgb(0, 0, 0)")]
[DataRow("CIELAB", "CIELab(0, 0, 0)")]
[DataRow("CIEXYZ", "XYZ(0, 0, 0)")]
[DataRow("CIELAB", "CIELab(0, 0, 0)")]
[DataRow("Oklab", "oklab(0, 0, 0)")]
[DataRow("Oklch", "oklch(0, 0, 0)")]
[DataRow("VEC4", "(0f, 0f, 0f, 1f)")]
[DataRow("Decimal", "0")]
[DataRow("HEX Int", "0xFF000000")]

View File

@@ -282,11 +282,11 @@ namespace PowerLauncher.ViewModel
if (options.SearchQueryTuningEnabled)
{
sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * options.SearchClickedItemWeight))).ToList();
sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(options.SearchClickedItemWeight)).ToList();
}
else
{
sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * 5))).ToList();
sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(5)).ToList();
}
// remove history items in they are in the list as non-history items

View File

@@ -187,5 +187,20 @@ namespace Wox.Plugin
/// Gets plugin ID that generated this result
/// </summary>
public string PluginID { get; internal set; }
/// <summary>
/// Gets or sets a value indicating whether usage based sorting should be applied to this result.
/// </summary>
public bool DisableUsageBasedScoring { get; set; }
public int GetSortOrderScore(int selectedItemMultiplier)
{
if (DisableUsageBasedScoring)
{
return Metadata.WeightBoost + Score;
}
return Metadata.WeightBoost + Score + (SelectedCount * selectedItemMultiplier);
}
}
}

View File

@@ -4,9 +4,7 @@
using System;
using System.IO;
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
@@ -46,17 +44,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
{
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
if (Hotkey == null)
{
Hotkey = DefaultHotkeyValue;
}
}
}
catch (Exception)
{
Hotkey = DefaultHotkeyValue;
}
Hotkey ??= DefaultHotkeyValue;
}
}
}

View File

@@ -32,8 +32,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
VisibleColorFormats.Add("HSI", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HSI")));
VisibleColorFormats.Add("HWB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HWB")));
VisibleColorFormats.Add("NCol", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("NCol")));
VisibleColorFormats.Add("CIELAB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
VisibleColorFormats.Add("CIEXYZ", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIEXYZ")));
VisibleColorFormats.Add("CIELAB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
VisibleColorFormats.Add("Oklab", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Oklab")));
VisibleColorFormats.Add("Oklch", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Oklch")));
VisibleColorFormats.Add("VEC4", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("VEC4")));
VisibleColorFormats.Add("Decimal", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Decimal")));
VisibleColorFormats.Add("HEX Int", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HEX Int")));

View File

@@ -80,5 +80,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations
/// Color presentation as an 8-digit hexadecimal integer (0xFFFFFFFF)
/// </summary>
HexInteger = 13,
/// <summary>
/// Color representation as CIELCh color space (L[0..100], C[0..230], h[0°..360°])
/// </summary>
CIELCh = 14,
/// <summary>
/// Color representation as Oklab color space (L[0..1], a[-0.5..0.5], b[-0.5..0.5])
/// </summary>
Oklab = 15,
/// <summary>
/// Color representation as Oklch color space (L[0..1], C[0..0.5], h[0°..360°])
/// </summary>
Oklch = 16,
}
}

View File

@@ -32,7 +32,8 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Description}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
TextWrapping="NoWrap"
ToolTipService.ToolTip="{x:Bind Description}" />
</Grid>
</DataTemplate>

View File

@@ -47,12 +47,17 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
new ColorFormatParameter() { Parameter = "%In", Description = resourceLoader.GetString("Help_intensity") },
new ColorFormatParameter() { Parameter = "%Hn", Description = resourceLoader.GetString("Help_hueNat") },
new ColorFormatParameter() { Parameter = "%Ll", Description = resourceLoader.GetString("Help_lightnessNat") },
new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
new ColorFormatParameter() { Parameter = "%Va", Description = resourceLoader.GetString("Help_value") },
new ColorFormatParameter() { Parameter = "%Wh", Description = resourceLoader.GetString("Help_whiteness") },
new ColorFormatParameter() { Parameter = "%Bn", Description = resourceLoader.GetString("Help_blackness") },
new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityA") },
new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityB") },
new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityACIE") },
new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityBCIE") },
new ColorFormatParameter() { Parameter = "%Lo", Description = resourceLoader.GetString("Help_lightnessOklab") },
new ColorFormatParameter() { Parameter = "%Oa", Description = resourceLoader.GetString("Help_chromaticityAOklab") },
new ColorFormatParameter() { Parameter = "%Ob", Description = resourceLoader.GetString("Help_chromaticityBOklab") },
new ColorFormatParameter() { Parameter = "%Oc", Description = resourceLoader.GetString("Help_chromaOklch") },
new ColorFormatParameter() { Parameter = "%Oh", Description = resourceLoader.GetString("Help_hueOklch") },
new ColorFormatParameter() { Parameter = "%Xv", Description = resourceLoader.GetString("Help_X_value") },
new ColorFormatParameter() { Parameter = "%Yv", Description = resourceLoader.GetString("Help_Y_value") },
new ColorFormatParameter() { Parameter = "%Zv", Description = resourceLoader.GetString("Help_Z_value") },

View File

@@ -1660,11 +1660,11 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<data name="Help_blackness" xml:space="preserve">
<value>blackness</value>
</data>
<data name="Help_chromaticityA" xml:space="preserve">
<value>chromaticityA</value>
<data name="Help_chromaticityACIE" xml:space="preserve">
<value>chromaticity A (CIE Lab)</value>
</data>
<data name="Help_chromaticityB" xml:space="preserve">
<value>chromaticityB</value>
<data name="Help_chromaticityBCIE" xml:space="preserve">
<value>chromaticity B (CIE Lab)</value>
</data>
<data name="Help_X_value" xml:space="preserve">
<value>X value</value>
@@ -4460,7 +4460,7 @@ Activate by holding the key for the character you want to add an accent to, then
<data name="NewPlus_Behaviour_Replace_Variables_Info_Card_Title.Text" xml:space="preserve">
<value>Commonly used variables</value>
<comment>New+ commonly used variables header in the flyout info card</comment>
</data>
</data>
<data name="NewPlus_Year_YYYY_Variable_Description.Text" xml:space="preserve">
<value>Year, represented by a full four or five digits, depending on the calendar used.</value>
<comment>New+ description of the year $YYYY variable - casing of $YYYY is important</comment>
@@ -4999,4 +4999,25 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="CmdPal_ActivationShortcut.Description" xml:space="preserve">
<value>Go to Command Palette settings to customize the activation shortcut.</value>
</data>
<data name="Help_chromaCIE" xml:space="preserve">
<value>chroma (CIE LCh)</value>
</data>
<data name="Help_hueCIE" xml:space="preserve">
<value>hue (CIE LCh)</value>
</data>
<data name="Help_lightnessOklab" xml:space="preserve">
<value>lightness (Oklab/Oklch)</value>
</data>
<data name="Help_chromaticityAOklab" xml:space="preserve">
<value>chromaticity A (Oklab)</value>
</data>
<data name="Help_chromaticityBOklab" xml:space="preserve">
<value>chromaticity B (Oklab)</value>
</data>
<data name="Help_chromaOklch" xml:space="preserve">
<value>chroma (Oklch)</value>
</data>
<data name="Help_hueOklch" xml:space="preserve">
<value>hue (Oklch)</value>
</data>
</root>

View File

@@ -116,4 +116,44 @@ function EnsureCertificate {
}
return $cert
}
}
function Export-CertificateFiles {
param (
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[string]$CerPath,
[string]$PfxPath,
[securestring]$PfxPassword
)
if (-not $Certificate) {
Write-Error "No certificate provided to export."
return
}
if ($CerPath) {
try {
Export-Certificate -Cert $Certificate -FilePath $CerPath -Force | Out-Null
Write-Host "Exported CER to: $CerPath"
} catch {
Write-Warning "Failed to export CER file: $_"
}
}
if ($PfxPath -and $PfxPassword) {
try {
Export-PfxCertificate -Cert $Certificate -FilePath $PfxPath -Password $PfxPassword -Force | Out-Null
Write-Host "Exported PFX to: $PfxPath"
} catch {
Write-Warning "Failed to export PFX file: $_"
}
}
if (-not $CerPath -and -not $PfxPath) {
Write-Warning "No output path specified. Nothing was exported."
}
}
$cert = EnsureCertificate
$pswd = ConvertTo-SecureString -String "MySecurePassword123!" -AsPlainText -Force
Export-CertificateFiles -Certificate $cert -CerPath "$env:TEMP\cert.cer" -PfxPath "$env:TEMP\cert.pfx" -PfxPassword $pswd