Rename the [Ee]xts dir to ext (#38852)

**WARNING:** This PR will probably blow up all in-flight PRs

at some point in the early days of CmdPal, two of us created seperate
`Exts` and `exts` dirs. Depending on what the casing was on the branch
that you checked one of those out from, it'd get stuck like that on your
PC forever.

Windows didn't care, so we never noticed.

But GitHub does care, and now browsing the source on GitHub is basically
impossible.

Closes #38081
This commit is contained in:
Mike Griese
2025-04-15 06:07:22 -05:00
committed by GitHub
parent 60f50d853b
commit 2b5181b4c9
379 changed files with 35 additions and 35 deletions

View File

@@ -0,0 +1,230 @@
// 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.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Management.Deployment;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.WinGet.Pages;
public partial class InstallPackageCommand : InvokableCommand
{
private readonly CatalogPackage _package;
private readonly StatusMessage _installBanner = new();
private IAsyncOperationWithProgress<InstallResult, InstallProgress>? _installAction;
private IAsyncOperationWithProgress<UninstallResult, UninstallProgress>? _unInstallAction;
private Task? _installTask;
public bool IsInstalled { get; private set; }
public static IconInfo CompletedIcon { get; } = new("\uE930"); // Completed
public static IconInfo DownloadIcon { get; } = new("\uE896"); // Download
public static IconInfo DeleteIcon { get; } = new("\uE74D"); // Delete
public event EventHandler<InstallPackageCommand>? InstallStateChanged;
private static readonly CompositeFormat UninstallingPackage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstalling_package);
private static readonly CompositeFormat InstallingPackage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_installing_package);
private static readonly CompositeFormat InstallPackageFinished = System.Text.CompositeFormat.Parse(Properties.Resources.winget_install_package_finished);
private static readonly CompositeFormat UninstallPackageFinished = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstall_package_finished);
private static readonly CompositeFormat QueuedPackageDownload = System.Text.CompositeFormat.Parse(Properties.Resources.winget_queued_package_download);
private static readonly CompositeFormat InstallPackageFinishing = System.Text.CompositeFormat.Parse(Properties.Resources.winget_install_package_finishing);
private static readonly CompositeFormat QueuedPackageUninstall = System.Text.CompositeFormat.Parse(Properties.Resources.winget_queued_package_uninstall);
private static readonly CompositeFormat UninstallPackageFinishing = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstall_package_finishing);
private static readonly CompositeFormat DownloadProgress = System.Text.CompositeFormat.Parse(Properties.Resources.winget_download_progress);
internal bool SkipDependencies { get; set; }
public InstallPackageCommand(CatalogPackage package, bool isInstalled)
{
_package = package;
IsInstalled = isInstalled;
UpdateAppearance();
}
internal void FakeChangeStatus()
{
IsInstalled = !IsInstalled;
UpdateAppearance();
}
private void UpdateAppearance()
{
Icon = IsInstalled ? CompletedIcon : DownloadIcon;
Name = IsInstalled ? Properties.Resources.winget_uninstall_name : Properties.Resources.winget_install_name;
}
public override ICommandResult Invoke()
{
// TODO: LOCK in here, so this can only be invoked once until the
// install / uninstall is done. Just use like, an atomic
if (_installTask != null)
{
return CommandResult.KeepOpen();
}
if (IsInstalled)
{
// Uninstall
_installBanner.State = MessageState.Info;
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallingPackage, _package.Name);
WinGetExtensionHost.Instance.ShowStatus(_installBanner, StatusContext.Extension);
var installOptions = WinGetStatics.WinGetFactory.CreateUninstallOptions();
installOptions.PackageUninstallScope = PackageUninstallScope.Any;
_unInstallAction = WinGetStatics.Manager.UninstallPackageAsync(_package, installOptions);
var handler = new AsyncOperationProgressHandler<UninstallResult, UninstallProgress>(OnUninstallProgress);
_unInstallAction.Progress = handler;
_installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction));
}
else
{
// Install
_installBanner.State = MessageState.Info;
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallingPackage, _package.Name);
WinGetExtensionHost.Instance.ShowStatus(_installBanner, StatusContext.Extension);
var installOptions = WinGetStatics.WinGetFactory.CreateInstallOptions();
installOptions.PackageInstallScope = PackageInstallScope.Any;
installOptions.SkipDependencies = SkipDependencies;
_installAction = WinGetStatics.Manager.InstallPackageAsync(_package, installOptions);
var handler = new AsyncOperationProgressHandler<InstallResult, InstallProgress>(OnInstallProgress);
_installAction.Progress = handler;
_installTask = Task.Run(() => TryDoInstallOperation(_installAction));
}
return CommandResult.KeepOpen();
}
private async void TryDoInstallOperation<T_Operation, T_Progress>(
IAsyncOperationWithProgress<T_Operation, T_Progress> action)
{
try
{
await action.AsTask();
_installBanner.Message = IsInstalled ?
string.Format(CultureInfo.CurrentCulture, UninstallPackageFinished, _package.Name) :
string.Format(CultureInfo.CurrentCulture, InstallPackageFinished, _package.Name);
_installBanner.Progress = null;
_installBanner.State = MessageState.Success;
_installTask = null;
_ = Task.Run(() =>
{
Thread.Sleep(2500);
if (_installTask == null)
{
WinGetExtensionHost.Instance.HideStatus(_installBanner);
}
});
InstallStateChanged?.Invoke(this, this);
}
catch (Exception ex)
{
_installBanner.State = MessageState.Error;
_installBanner.Progress = null;
_installBanner.Message = ex.Message;
_installTask = null;
}
}
private static string FormatBytes(ulong bytes)
{
const long KB = 1024;
const long MB = KB * 1024;
const long GB = MB * 1024;
return bytes >= GB
? $"{bytes / (double)GB:F2} GB"
: bytes >= MB ?
$"{bytes / (double)MB:F2} MB"
: bytes >= KB
? $"{bytes / (double)KB:F2} KB"
: $"{bytes} bytes";
}
private void OnInstallProgress(
IAsyncOperationWithProgress<InstallResult, InstallProgress> operation,
InstallProgress progress)
{
switch (progress.State)
{
case PackageInstallProgressState.Queued:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, QueuedPackageDownload, _package.Name);
break;
case PackageInstallProgressState.Downloading:
if (progress.BytesRequired > 0)
{
var downloadText = string.Format(CultureInfo.CurrentCulture, DownloadProgress, FormatBytes(progress.BytesDownloaded), FormatBytes(progress.BytesRequired));
_installBanner.Progress ??= new ProgressState() { IsIndeterminate = false };
var downloaded = progress.BytesDownloaded / (float)progress.BytesRequired;
var percent = downloaded * 100.0f;
((ProgressState)_installBanner.Progress).ProgressPercent = (uint)percent;
_installBanner.Message = downloadText;
}
break;
case PackageInstallProgressState.Installing:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallingPackage, _package.Name);
_installBanner.Progress = new ProgressState() { IsIndeterminate = true };
break;
case PackageInstallProgressState.PostInstall:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallPackageFinishing, _package.Name);
break;
case PackageInstallProgressState.Finished:
_installBanner.Message = Properties.Resources.winget_install_finished;
// progressBar.IsIndeterminate(false);
_installBanner.Progress = null;
_installBanner.State = MessageState.Success;
break;
default:
_installBanner.Message = string.Empty;
break;
}
}
private void OnUninstallProgress(
IAsyncOperationWithProgress<UninstallResult, UninstallProgress> operation,
UninstallProgress progress)
{
switch (progress.State)
{
case PackageUninstallProgressState.Queued:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, QueuedPackageUninstall, _package.Name);
break;
case PackageUninstallProgressState.Uninstalling:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallingPackage, _package.Name);
_installBanner.Progress = new ProgressState() { IsIndeterminate = true };
break;
case PackageUninstallProgressState.PostUninstall:
_installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallPackageFinishing, _package.Name);
break;
case PackageUninstallProgressState.Finished:
_installBanner.Message = Properties.Resources.winget_uninstall_finished;
_installBanner.Progress = null;
_installBanner.State = MessageState.Success;
break;
default:
_installBanner.Message = string.Empty;
break;
}
}
}

View File

@@ -0,0 +1,225 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Management.Deployment;
using Windows.Foundation.Metadata;
namespace Microsoft.CmdPal.Ext.WinGet.Pages;
public partial class InstallPackageListItem : ListItem
{
private readonly CatalogPackage _package;
// Lazy-init the details
private readonly Lazy<Details?> _details;
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
private InstallPackageCommand? _installCommand;
public InstallPackageListItem(CatalogPackage package)
: base(new NoOpCommand())
{
_package = package;
var version = _package.DefaultInstallVersion;
var versionTagText = "Unknown";
if (version != null)
{
versionTagText = version.Version == "Unknown" && version.PackageCatalog.Info.Id == "StoreEdgeFD" ? "msstore" : version.Version;
}
Title = _package.Name;
Subtitle = _package.Id;
Tags = [new Tag() { Text = versionTagText }];
_details = new Lazy<Details?>(() => BuildDetails(version));
_ = Task.Run(UpdatedInstalledStatus);
}
private Details? BuildDetails(PackageVersionInfo? version)
{
var metadata = version?.GetCatalogPackageMetadata();
if (metadata != null)
{
if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any())
{
if (_installCommand != null)
{
_installCommand.SkipDependencies = true;
}
}
var description = string.IsNullOrEmpty(metadata.Description) ? metadata.ShortDescription : metadata.Description;
var detailsBody = $"""
{description}
""";
IconInfo heroIcon = new(string.Empty);
var icons = metadata.Icons;
if (icons.Count > 0)
{
// There's also a .Theme property we could probably use to
// switch between default or individual icons.
heroIcon = new IconInfo(icons[0].Url);
}
return new Details()
{
Body = detailsBody,
Title = metadata.PackageName,
HeroImage = heroIcon,
Metadata = GetDetailsMetadata(metadata).ToArray(),
};
}
return null;
}
private List<IDetailsElement> GetDetailsMetadata(CatalogPackageMetadata metadata)
{
List<IDetailsElement> detailsElements = [];
// key -> {text, url}
Dictionary<string, (string, string)> simpleData = new()
{
{ Properties.Resources.winget_author, (metadata.Author, string.Empty) },
{ Properties.Resources.winget_publisher, (metadata.Publisher, metadata.PublisherUrl) },
{ Properties.Resources.winget_copyright, (metadata.Copyright, metadata.CopyrightUrl) },
{ Properties.Resources.winget_license, (metadata.License, metadata.LicenseUrl) },
{ Properties.Resources.winget_publisher_support, (string.Empty, metadata.PublisherSupportUrl) },
// The link to the release notes will only show up if there is an
// actual URL for the release notes
{ Properties.Resources.winget_view_release_notes, (string.IsNullOrEmpty(metadata.ReleaseNotesUrl) ? string.Empty : Properties.Resources.winget_view_online, metadata.ReleaseNotesUrl) },
// These can be l o n g
{ Properties.Resources.winget_release_notes, (metadata.ReleaseNotes, string.Empty) },
};
var docs = metadata.Documentations.ToArray();
foreach (var item in docs)
{
simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl));
}
UriCreationOptions options = default;
foreach (var kv in simpleData)
{
var text = string.IsNullOrEmpty(kv.Value.Item1) ? kv.Value.Item2 : kv.Value.Item1;
var target = kv.Value.Item2;
if (!string.IsNullOrEmpty(text))
{
Uri? uri = null;
Uri.TryCreate(target, options, out uri);
DetailsElement pair = new()
{
Key = kv.Key,
Data = new DetailsLink() { Link = uri, Text = text },
};
detailsElements.Add(pair);
}
}
if (metadata.Tags.Any())
{
DetailsElement pair = new()
{
Key = "Tags",
Data = new DetailsTags() { Tags = metadata.Tags.Select(t => new Tag(t)).ToArray() },
};
detailsElements.Add(pair);
}
return detailsElements;
}
private async void UpdatedInstalledStatus()
{
var status = await _package.CheckInstalledStatusAsync();
var isInstalled = _package.InstalledVersion != null;
// might be an uninstall command
InstallPackageCommand installCommand = new(_package, isInstalled);
if (isInstalled)
{
this.Icon = InstallPackageCommand.CompletedIcon;
this.Command = new NoOpCommand();
List<IContextItem> contextMenu = [];
CommandContextItem uninstallContextItem = new(installCommand)
{
IsCritical = true,
Icon = InstallPackageCommand.DeleteIcon,
};
if (WinGetStatics.AppSearchCallback != null)
{
var callback = WinGetStatics.AppSearchCallback;
var installedApp = callback(_package.DefaultInstallVersion == null ? _package.Name : _package.DefaultInstallVersion.DisplayName);
if (installedApp != null)
{
this.Command = installedApp.Command;
contextMenu = [.. installedApp.MoreCommands];
}
}
contextMenu.Add(uninstallContextItem);
this.MoreCommands = contextMenu.ToArray();
return;
}
// didn't find the app
_installCommand = new InstallPackageCommand(_package, isInstalled);
this.Command = _installCommand;
Icon = _installCommand.Icon;
_installCommand.InstallStateChanged += InstallStateChangedHandler;
}
private void InstallStateChangedHandler(object? sender, InstallPackageCommand e)
{
if (!ApiInformation.IsApiContractPresent("Microsoft.Management.Deployment.WindowsPackageManagerContract", 12))
{
Logger.LogError($"RefreshPackageCatalogAsync isn't available");
e.FakeChangeStatus();
Command = e;
Icon = (IconInfo?)Command.Icon;
return;
}
_ = Task.Run(() =>
{
Stopwatch s = new();
Logger.LogDebug($"Starting RefreshPackageCatalogAsync");
s.Start();
var refs = WinGetStatics.AvailableCatalogs.ToArray();
foreach (var catalog in refs)
{
var operation = catalog.RefreshPackageCatalogAsync();
operation.Wait();
}
s.Stop();
Logger.LogDebug($"RefreshPackageCatalogAsync took {s.ElapsedMilliseconds}ms");
}).ContinueWith((previous) =>
{
if (previous.IsCompletedSuccessfully)
{
Logger.LogDebug($"Updating InstalledStatus");
UpdatedInstalledStatus();
}
});
}
}

View File

@@ -0,0 +1,271 @@
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WinGet.Pages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Management.Deployment;
namespace Microsoft.CmdPal.Ext.WinGet;
internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
{
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_unexpected_error);
private readonly string _tag = string.Empty;
public bool HasTag => !string.IsNullOrEmpty(_tag);
private readonly Lock _resultsLock = new();
private CancellationTokenSource? _cancellationTokenSource;
private Task<IEnumerable<CatalogPackage>>? _currentSearchTask;
private IEnumerable<CatalogPackage>? _results;
public static IconInfo WinGetIcon { get; } = IconHelpers.FromRelativePath("Assets\\WinGet.svg");
public static IconInfo ExtensionsIcon { get; } = IconHelpers.FromRelativePath("Assets\\Extension.svg");
public static string ExtensionsTag => "windows-commandpalette-extension";
private readonly StatusMessage _errorMessage = new() { State = MessageState.Error };
public WinGetExtensionPage(string tag = "")
{
Icon = tag == ExtensionsTag ? ExtensionsIcon : WinGetIcon;
Name = Properties.Resources.winget_page_name;
_tag = tag;
ShowDetails = true;
}
public override IListItem[] GetItems()
{
IListItem[] items = [];
lock (_resultsLock)
{
// emptySearchForTag ===
// we don't have results yet, we haven't typed anything, and we're searching for a tag
bool emptySearchForTag = _results == null &&
string.IsNullOrEmpty(SearchText) &&
HasTag;
if (emptySearchForTag)
{
IsLoading = true;
DoUpdateSearchText(string.Empty);
return items;
}
if (_results != null && _results.Any())
{
ListItem[] results = _results.Select(PackageToListItem).ToArray();
IsLoading = false;
return results;
}
}
EmptyContent = new CommandItem(new NoOpCommand())
{
Icon = WinGetIcon,
Title = (string.IsNullOrEmpty(SearchText) && !HasTag) ?
Properties.Resources.winget_placeholder_text :
Properties.Resources.winget_no_packages_found,
};
IsLoading = false;
return items;
}
private static ListItem PackageToListItem(CatalogPackage p) => new InstallPackageListItem(p);
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (newSearch == oldSearch)
{
return;
}
DoUpdateSearchText(newSearch);
}
private void DoUpdateSearchText(string newSearch)
{
// Cancel any ongoing search
if (_cancellationTokenSource != null)
{
Logger.LogDebug("Cancelling old search", memberName: nameof(DoUpdateSearchText));
_cancellationTokenSource.Cancel();
}
_cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = _cancellationTokenSource.Token;
IsLoading = true;
// Save the latest search task
_currentSearchTask = DoSearchAsync(newSearch, cancellationToken);
// Await the task to ensure only the latest one gets processed
_ = ProcessSearchResultsAsync(_currentSearchTask, newSearch);
}
private async Task ProcessSearchResultsAsync(
Task<IEnumerable<CatalogPackage>> searchTask,
string newSearch)
{
try
{
IEnumerable<CatalogPackage> results = await searchTask;
// Ensure this is still the latest task
if (_currentSearchTask == searchTask)
{
// Process the results (e.g., update UI)
UpdateWithResults(results, newSearch);
}
}
catch (OperationCanceledException)
{
// Handle cancellation gracefully (e.g., log or ignore)
Logger.LogDebug($" Cancelled search for '{newSearch}'");
}
catch (Exception ex)
{
// Handle other exceptions
Logger.LogError(ex.Message);
}
}
private void UpdateWithResults(IEnumerable<CatalogPackage> results, string query)
{
Logger.LogDebug($"Completed search for '{query}'");
lock (_resultsLock)
{
this._results = results;
}
RaiseItemsChanged(this._results.Count());
}
private async Task<IEnumerable<CatalogPackage>> DoSearchAsync(string query, CancellationToken ct)
{
// Were we already canceled?
ct.ThrowIfCancellationRequested();
Stopwatch stopwatch = new();
stopwatch.Start();
if (string.IsNullOrEmpty(query)
&& string.IsNullOrEmpty(_tag))
{
return [];
}
string searchDebugText = $"{query}{(HasTag ? "+" : string.Empty)}{_tag}";
Logger.LogDebug($"Starting search for '{searchDebugText}'");
HashSet<CatalogPackage> results = new(new PackageIdCompare());
// Default selector: this is the way to do a `winget search <query>`
PackageMatchFilter selector = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
selector.Field = Microsoft.Management.Deployment.PackageMatchField.CatalogDefault;
selector.Value = query;
selector.Option = PackageFieldMatchOption.ContainsCaseInsensitive;
FindPackagesOptions opts = WinGetStatics.WinGetFactory.CreateFindPackagesOptions();
opts.Selectors.Add(selector);
// testing
opts.ResultLimit = 25;
// Selectors is "OR", Filters is "AND"
if (HasTag)
{
PackageMatchFilter tagFilter = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
tagFilter.Field = Microsoft.Management.Deployment.PackageMatchField.Tag;
tagFilter.Value = _tag;
tagFilter.Option = PackageFieldMatchOption.ContainsCaseInsensitive;
opts.Filters.Add(tagFilter);
}
// Clean up here, then...
ct.ThrowIfCancellationRequested();
Lazy<Task<PackageCatalog>> catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog;
// Both these catalogs should have been instantiated by the
// WinGetStatics static ctor when we were created.
PackageCatalog catalog = await catalogTask.Value;
if (catalog == null)
{
// This error should have already been displayed by WinGetStatics
return [];
}
// foreach (var catalog in connections)
{
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
ct.ThrowIfCancellationRequested();
// BODGY, re: microsoft/winget-cli#5151
// FindPackagesAsync isn't actually async.
Task<FindPackagesResult> internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
FindPackagesResult searchResults = await internalSearchTask;
// TODO more error handling like this:
if (searchResults.Status != FindPackagesResultStatus.Ok)
{
_errorMessage.Message = string.Format(CultureInfo.CurrentCulture, ErrorMessage, searchResults.Status);
WinGetExtensionHost.Instance.ShowStatus(_errorMessage, StatusContext.Page);
return [];
}
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
foreach (Management.Deployment.MatchResult? match in searchResults.Matches.ToArray())
{
ct.ThrowIfCancellationRequested();
// Print the packages
CatalogPackage package = match.CatalogPackage;
results.Add(package);
}
Logger.LogDebug($" ({searchDebugText}): count: {results.Count}", memberName: nameof(DoSearchAsync));
}
stopwatch.Stop();
Logger.LogDebug($"Search \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
return results;
}
public void Dispose() => throw new NotImplementedException();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "I just like it")]
public sealed class PackageIdCompare : IEqualityComparer<CatalogPackage>
{
public bool Equals(CatalogPackage? x, CatalogPackage? y) =>
(x?.Id == y?.Id)
&& (x?.DefaultInstallVersion?.PackageCatalog == y?.DefaultInstallVersion?.PackageCatalog);
public int GetHashCode([DisallowNull] CatalogPackage obj) => obj.Id.GetHashCode();
}