Compare commits

...

2 Commits

Author SHA1 Message Date
Mike Griese
acbd438426 PRE-MERGE #38844 2025-05-04 06:09:09 -05:00
Mike Griese
dd111841fa PRE-MERGE #39170 2025-05-04 06:08:28 -05:00
40 changed files with 1062 additions and 109 deletions

View File

@@ -198,6 +198,7 @@ CLIPBOARDUPDATE
CLIPCHILDREN
CLIPSIBLINGS
closesocket
clp
CLSCTX
clsids
Clusion
@@ -1045,6 +1046,7 @@ NOINHERITLAYOUT
NOINTERFACE
NOINVERT
NOLINKINFO
nologo
NOMCX
NOMINMAX
NOMIRRORBITMAP
@@ -1277,6 +1279,7 @@ pstm
PStr
pstream
pstrm
pswd
PSYSTEM
psz
ptb
@@ -1423,6 +1426,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1978,4 +1982,12 @@ zoomit
ZOOMITX
ZXk
ZXNs
zzz
zzz
ACIE
AOklab
BCIE
BOklab
culori
Evercoder
LCh
CIELCh

View File

@@ -79,10 +79,7 @@
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
<ComponentGroupRef Id="WorkspacesComponentGroup" />
<?if $(var.CIBuild) = "true" ?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
<?endif?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />

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

@@ -301,6 +301,7 @@ namespace package
if (!std::filesystem::exists(directoryPath))
{
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
return {};
}
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);

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,6 +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')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
@@ -49,13 +50,21 @@
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
%(PreprocessorDefinitions);
$(CommandPaletteBranding)
</PreprocessorDefinitions>
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
IS_DEV_BRANDING;%(PreprocessorDefinitions)
</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />

View File

@@ -208,7 +208,7 @@ public:
try
{
std::wstring packageName = L"Microsoft.CommandPalette";
#ifdef _DEBUG
#ifdef IS_DEV_BRANDING
packageName = L"Microsoft.CommandPalette.Dev";
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
@@ -245,12 +245,21 @@ public:
errorMessage += e.what();
Logger::error(errorMessage);
}
#if _DEBUG
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
try
{
#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);
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
#endif
}
catch (std::exception& e)
{
std::string errorMessage{ "Exception thrown while trying to launch CmdPal: " };
errorMessage += e.what();
Logger::error(errorMessage);
throw;
}
}
virtual void disable()

View File

@@ -66,8 +66,10 @@ public sealed class CommandProviderWrapper
DisplayName = provider.DisplayName;
Icon = new(provider.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(provider.Settings, this, _taskScheduler);
Settings.InitializeProperties();
Logger.LogDebug($"Initialized command provider {ProviderId}");
}
@@ -151,9 +153,11 @@ public sealed class CommandProviderWrapper
Icon = new(model.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(model.Settings, this, _taskScheduler);
Settings.InitializeProperties();
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
@@ -194,21 +198,6 @@ public sealed class CommandProviderWrapper
}
}
/* This is a View/ExtensionHost piece
* public void AllowSetForeground(bool allow)
{
if (!IsExtension)
{
return;
}
var iextn = extensionWrapper?.GetExtensionObject();
unsafe
{
PInvoke.CoAllowSetForegroundWindow(iextn);
}
}*/
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();

View File

@@ -2,18 +2,25 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
{
private readonly ExtensionObject<ICommandSettings> _model = new(_unsafeSettings);
public ContentPageViewModel? SettingsPage { get; private set; }
public void InitializeProperties()
public bool Initialized { get; private set; }
public bool HasSettings =>
_model.Unsafe != null && // We have a settings model AND
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
private void UnsafeInitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
@@ -27,4 +34,27 @@ public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings,
SettingsPage.InitializeProperties();
}
}
public void SafeInitializeProperties()
{
try
{
UnsafeInitializeProperties();
}
catch (Exception ex)
{
Logger.LogError($"Failed to load settings page", ex: ex);
}
Initialized = true;
}
public void DoOnUiThread(Action action)
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
mainThread);
}
}

View File

@@ -18,6 +18,8 @@ public partial class ProviderSettingsViewModel(
IServiceProvider _serviceProvider) : ObservableObject
{
private readonly SettingsModel _settings = _serviceProvider.GetService<SettingsModel>()!;
private readonly Lock _initializeSettingsLock = new();
private Task? _initializeSettingsTask;
public string DisplayName => _provider.DisplayName;
@@ -34,6 +36,9 @@ public partial class ProviderSettingsViewModel(
public IconInfoViewModel Icon => _provider.Icon;
[ObservableProperty]
public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false;
public bool IsEnabled
{
get => _providerSettings.IsEnabled;
@@ -56,15 +61,60 @@ public partial class ProviderSettingsViewModel(
}
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
/// <summary>
/// Gets a value indicating whether returns true if we have a settings page
/// that's initialized, or we are still working on initializing that
/// settings page. If we don't have a settings object, or that settings
/// object doesn't have a settings page, then we'll return false.
/// </summary>
public bool HasSettings
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
get
{
if (_provider.Settings == null)
{
return false;
}
if (_provider.Settings.Initialized)
{
return _provider.Settings.HasSettings;
}
// settings still need to be loaded.
return LoadingSettings;
}
}
public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null;
/// <summary>
/// Gets will return the settings page, if we have one, and have initialized it.
/// If we haven't initialized it, this will kick off a thread to start
/// initializing it.
/// </summary>
public ContentPageViewModel? SettingsPage
{
get
{
if (_provider.Settings == null)
{
return null;
}
public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null;
if (_provider.Settings.Initialized)
{
LoadingSettings = false;
return _provider.Settings.SettingsPage;
}
// Don't load the settings if we're already working on it
lock (_initializeSettingsLock)
{
_initializeSettingsTask ??= Task.Run(InitializeSettingsPage);
}
return null;
}
}
[field: AllowNull]
public List<TopLevelViewModel> TopLevelCommands
@@ -90,4 +140,30 @@ public partial class ProviderSettingsViewModel(
}
private void Save() => SettingsModel.SaveSettings(_settings);
private void InitializeSettingsPage()
{
if (_provider.Settings == null)
{
return;
}
_provider.Settings.SafeInitializeProperties();
_provider.Settings.DoOnUiThread(() =>
{
// Changing these properties will try to update XAML, and that has
// to be handled on the UI thread, so we need to raise them on the
// UI thread
LoadingSettings = false;
OnPropertyChanged(nameof(HasSettings));
OnPropertyChanged(nameof(LoadingSettings));
OnPropertyChanged(nameof(SettingsPage));
});
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -44,6 +45,9 @@ public partial class TopLevelCommandManager : ObservableObject,
public async Task<bool> LoadBuiltinsAsync()
{
var s = new Stopwatch();
s.Start();
_builtInCommands.Clear();
// Load built-In commands first. These are all in-proc, and
@@ -56,6 +60,10 @@ public partial class TopLevelCommandManager : ObservableObject,
await LoadTopLevelCommandsFromProvider(wrapper);
}
s.Stop();
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
return true;
}
@@ -239,6 +247,9 @@ public partial class TopLevelCommandManager : ObservableObject,
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
{
var s = new Stopwatch();
s.Start();
// TODO This most definitely needs a lock
foreach (var extension in extensions)
{
@@ -258,6 +269,10 @@ public partial class TopLevelCommandManager : ObservableObject,
Logger.LogError(ex.ToString());
}
}
s.Stop();
Logger.LogDebug($"Loading extensions took {s.ElapsedMilliseconds}ms");
}
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)

View File

@@ -25,6 +25,7 @@ using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
@@ -232,6 +233,16 @@ public sealed partial class MainWindow : Window,
PositionCentered(display);
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
// instead of showing the window, uncloak it from DWM
// This will make it visible to the user, without the animation or frames for
// loading XAML with composition
unsafe
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, (void*)&value, (uint)sizeof(BOOL));
}
PInvoke.SetForegroundWindow(hwnd);
PInvoke.SetActiveWindow(hwnd);
}
@@ -289,7 +300,7 @@ public sealed partial class MainWindow : Window,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
public void Receive(HideWindowMessage message) => HideWindow();
public void Receive(QuitMessage message) =>
@@ -297,7 +308,21 @@ public sealed partial class MainWindow : Window,
DispatcherQueue.TryEnqueue(() => Close());
public void Receive(DismissMessage message) =>
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
HideWindow();
private void HideWindow()
{
// Hide our window
// Instead of hiding the window, cloak it from DWM
// This will make it invisible to the user, such that we can show it again
// by uncloaking it, which avoids an unnecessary "flicker in" that XAML does
unsafe
{
BOOL value = true;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, (void*)&value, (uint)sizeof(BOOL));
}
}
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
@@ -386,7 +411,9 @@ public sealed partial class MainWindow : Window,
return;
}
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// This will DWM cloak our window:
HideWindow();
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
@@ -479,10 +506,24 @@ public sealed partial class MainWindow : Window,
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
var isVisible = this.Visible;
unsafe
{
// We need to check if our window is cloaked or not. A cloaked window is still
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
// (these are all separate states)
long attr = 0;
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
if (attr == 1 /* DWM_CLOAKED_APP */)
{
isVisible = false;
}
}
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!this.Visible || !isRootHotkey)
if (!isVisible || !isRootHotkey)
{
Activate();
@@ -490,7 +531,16 @@ public sealed partial class MainWindow : Window,
}
else if (isRootHotkey)
{
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
return;
}
HideWindow();
}
}
@@ -518,22 +568,6 @@ public sealed partial class MainWindow : Window,
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
// // Note to future us: the wParam will have the index of the hotkey we registered.
// // We can use that in the future to differentiate the hotkeys we've pressed
// // so that we can bind hotkeys to individual commands
// if (!this.Visible || !isRootHotkey)
// {
// Activate();
// Summon(hotkey.CommandId);
// }
// else if (isRootHotkey)
// {
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// }
}
return (LRESULT)IntPtr.Zero;

View File

@@ -36,3 +36,7 @@ ExtractIconEx
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
DwmGetWindowAttribute
DwmSetWindowAttribute
DWM_CLOAKED_APP

View File

@@ -113,7 +113,24 @@
Visibility="{x:Bind ViewModel.HasSettings}" />
<Frame x:Name="SettingsFrame" Visibility="{x:Bind ViewModel.HasSettings}">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
<controls:SwitchPresenter
HorizontalAlignment="Stretch"
TargetType="x:Boolean"
Value="{x:Bind ViewModel.LoadingSettings, Mode=OneWay}">
<controls:Case Value="True">
<ProgressRing
Width="36"
Height="36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True" />
</controls:Case>
<controls:Case Value="False">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
</controls:Case>
</controls:SwitchPresenter>
</Frame>
<TextBlock

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

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

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

@@ -0,0 +1,122 @@
<#
.SYNOPSIS
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
.DESCRIPTION
This script automates the end-to-end build and packaging process for PowerToys, including:
- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.)
- Cleaning up old output
- Signing generated .msix packages
- Building the WiX-based MSI and bootstrapper installers
It is designed to work in local development.
.PARAMETER Platform
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'.
.PARAMETER Configuration
Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
.EXAMPLE
.\build-installer.ps1
Runs the installer build pipeline for ARM64 Release (default).
.EXAMPLE
.\build-installer.ps1 -Platform x64 -Configuration Release
Runs the pipeline for x64 Debug.
.NOTES
- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment.
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
- Generated MSIX files will be signed using cert-sign-package.ps1.
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
- First time run need admin permission to trust the certificate.
- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup
relative to the solution root directory.
- The installer can't be run right after the build, I need to copy it to another file before it can be run.
#>
param (
[string]$Platform = 'arm64',
[string]$Configuration = 'Release'
)
$repoRoot = Resolve-Path "$PSScriptRoot\..\.."
Set-Location $repoRoot
function RunMSBuild {
param (
[string]$Solution,
[string]$ExtraArgs
)
$base = @(
$Solution
"/p:Platform=`"$Platform`""
"/p:Configuration=$Configuration"
'/verbosity:normal'
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
'/nologo'
)
$cmd = $base + ($ExtraArgs -split ' ')
Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' '))
& msbuild.exe @cmd
if ($LASTEXITCODE -ne 0) {
Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs)
exit $LASTEXITCODE
}
}
function RestoreThenBuild {
param ([string]$Solution)
# 1) restore
RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true'
# 2) build -------------------------------------------------
RunMSBuild $Solution '/m'
}
Write-Host ("Make sure wix is installed and available")
& "$PSScriptRoot\ensure-wix.ps1"
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration)
Write-Host ''
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
if (Test-Path $cmdpalOutputPath) {
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
}
RestoreThenBuild '.\PowerToys.sln'
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
Select-Object -ExpandProperty FullName
if ($msixFiles.Count) {
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
& "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles
}
else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln'
RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln'
Write-Host '[CLEAN] installer (keep *.exe)'
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true'
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true'
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true'
Write-Host '[PIPELINE] Completed'

View File

@@ -0,0 +1,159 @@
<#
.SYNOPSIS
Ensures a code signing certificate exists and is trusted in all necessary certificate stores.
.DESCRIPTION
This script provides two functions:
1. EnsureCertificate:
- Searches for an existing code signing certificate by subject name.
- If not found, creates a new self-signed certificate.
- Exports the certificate and attempts to import it into:
- CurrentUser\TrustedPeople
- CurrentUser\Root
- LocalMachine\Root (admin privileges may be required)
2. ImportAndVerifyCertificate:
- Imports a `.cer` file into the specified certificate store if not already present.
- Verifies the certificate is successfully imported by checking thumbprint.
This is useful in build or signing pipelines to ensure a valid and trusted certificate is available before signing MSIX or executable files.
.PARAMETER certSubject
The subject name of the certificate to search for or create. Default is:
"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
.PARAMETER cerPath
(ImportAndVerifyCertificate only) The file path to a `.cer` certificate file to import.
.PARAMETER storePath
(ImportAndVerifyCertificate only) The destination certificate store path (e.g. Cert:\CurrentUser\Root).
.EXAMPLE
$cert = EnsureCertificate
Ensures the default certificate exists and is trusted, and returns the certificate object.
.EXAMPLE
ImportAndVerifyCertificate -cerPath "$env:TEMP\temp_cert.cer" -storePath "Cert:\CurrentUser\Root"
Imports a certificate into the CurrentUser Root store and verifies its presence.
.NOTES
- For full trust, administrative privileges may be needed to import into LocalMachine\Root.
- Certificates are created using RSA and SHA256 and marked as CodeSigningCert.
#>
function ImportAndVerifyCertificate {
param (
[string]$cerPath,
[string]$storePath
)
$thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint
$existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
if ($existingCert) {
Write-Host "Certificate already exists in $storePath"
return $true
}
try {
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
} catch {
Write-Warning "Failed to import certificate to $storePath : $_"
return $false
}
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
if ($imported) {
Write-Host "Certificate successfully imported to $storePath"
return $true
} else {
Write-Warning "Certificate not found in $storePath after import"
return $false
}
}
function EnsureCertificate {
param (
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
)
$cert = Get-ChildItem -Path Cert:\CurrentUser\My |
Where-Object { $_.Subject -eq $certSubject } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if (-not $cert) {
Write-Host "Certificate not found. Creating a new one..."
$cert = New-SelfSignedCertificate -Subject $certSubject `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyAlgorithm RSA `
-Type CodeSigningCert `
-HashAlgorithm SHA256
if (-not $cert) {
Write-Error "Failed to create a new certificate."
return $null
}
Write-Host "New certificate created with thumbprint: $($cert.Thumbprint)"
}
else {
Write-Host "Using existing certificate with thumbprint: $($cert.Thumbprint)"
}
$cerPath = "$env:TEMP\temp_cert.cer"
[void](Export-Certificate -Cert $cert -FilePath $cerPath -Force)
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) { return $null }
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) { return $null }
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) {
Write-Warning "Failed to import to LocalMachine\Root (admin may be required)"
return $null
}
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

View File

@@ -0,0 +1,29 @@
param (
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
[string[]]$TargetPaths = "C:\PowerToys\ARM64\Release\WinUI3Apps\CmdPal\AppPackages\Microsoft.CmdPal.UI_0.0.1.0_Test\Microsoft.CmdPal.UI_0.0.1.0_arm64.msix"
)
. "$PSScriptRoot\cert-management.ps1"
$cert = EnsureCertificate -certSubject $certSubject
if (-not $cert) {
Write-Error "Failed to prepare certificate."
exit 1
}
Write-Host "Certificate ready: $($cert.Thumbprint)"
if (-not $TargetPaths -or $TargetPaths.Count -eq 0) {
Write-Error "No target files provided to sign."
exit 1
}
foreach ($filePath in $TargetPaths) {
if (-not (Test-Path $filePath)) {
Write-Warning "Skipping: File does not exist - $filePath"
continue
}
Write-Host "Signing: $filePath"
& signtool sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com "$filePath"
}

View File

@@ -0,0 +1,71 @@
<#
.SYNOPSIS
Ensure WiX Toolset 3.14 (build 3141) is installed and ready to use.
.DESCRIPTION
- Skips installation if the toolset is already installed (unless -Force is used).
- Otherwise downloads the official installer and binaries, verifies SHA-256, installs silently,
and copies wix.targets into the installation directory.
.PARAMETER Force
Forces reinstallation even if the toolset is already detected.
.PARAMETER InstallDir
The target installation path. Default is 'C:\Program Files (x86)\WiX Toolset v3.14'.
.EXAMPLE
.\EnsureWix.ps1 # Ensure WiX is installed
.\EnsureWix.ps1 -Force # Force reinstall
#>
[CmdletBinding()]
param(
[switch]$Force,
[string]$InstallDir = 'C:\Program Files (x86)\WiX Toolset v3.14'
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# Download URLs and expected SHA-256 hashes
$WixDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe'
$WixBinariesDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip'
$InstallerHashExpected = '6BF6D03D6923D9EF827AE1D943B90B42B8EBB1B0F68EF6D55F868FA34C738A29'
$BinariesHashExpected = '6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31'
# Check if WiX is already installed
$candlePath = Join-Path $InstallDir 'bin\candle.exe'
if (-not $Force -and (Test-Path $candlePath)) {
Write-Host "WiX Toolset is already installed at `"$InstallDir`". Skipping installation."
return
}
# Temp file paths
$tmpDir = [IO.Path]::GetTempPath()
$installer = Join-Path $tmpDir 'wix314.exe'
$binariesZip = Join-Path $tmpDir 'wix314-binaries.zip'
# Download installer and binaries
Write-Host 'Downloading WiX installer...'
Invoke-WebRequest -Uri $WixDownloadUrl -OutFile $installer -UseBasicParsing
Write-Host 'Downloading WiX binaries...'
Invoke-WebRequest -Uri $WixBinariesDownloadUrl -OutFile $binariesZip -UseBasicParsing
# Verify SHA-256 hashes
Write-Host 'Verifying installer hash...'
if ((Get-FileHash -Algorithm SHA256 $installer).Hash -ne $InstallerHashExpected) {
throw 'wix314.exe SHA256 hash mismatch'
}
Write-Host 'Verifying binaries hash...'
if ((Get-FileHash -Algorithm SHA256 $binariesZip).Hash -ne $BinariesHashExpected) {
throw 'wix314-binaries.zip SHA256 hash mismatch'
}
# Perform silent installation
Write-Host 'Installing WiX Toolset silently...'
Start-Process -FilePath $installer -ArgumentList '/install','/quiet' -Wait
# Extract binaries and copy wix.targets
$expandDir = Join-Path $tmpDir 'wix-binaries'
if (Test-Path $expandDir) { Remove-Item $expandDir -Recurse -Force }
Expand-Archive -Path $binariesZip -DestinationPath $expandDir -Force
Copy-Item -Path (Join-Path $expandDir 'wix.targets') `
-Destination (Join-Path $InstallDir 'wix.targets') -Force
Write-Host "WiX Toolset has been successfully installed at: $InstallDir"