From 46a8f30f1993842ccd97685ca7db5f15e8c97a4c Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 10 Dec 2025 18:06:08 -0800 Subject: [PATCH 1/7] infra work and initial tiny details output --- src/AppInstallerCLICore/Argument.cpp | 2 + .../Commands/ListCommand.cpp | 1 + src/AppInstallerCLICore/ExecutionArgs.h | 1 + src/AppInstallerCLICore/Resources.h | 1 + .../Workflows/WorkflowBase.cpp | 51 ++++++++++++++++--- .../Shared/Strings/en-us/winget.resw | 6 ++- .../ExperimentalFeature.cpp | 4 ++ .../Public/winget/ExperimentalFeature.h | 1 + .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + 10 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 9263b35b41..0f608a1280 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -478,6 +478,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::FontDetailsArgumentDescription, ArgumentType::Flag, false }; case Args::Type::Correlation: return Argument{ type, Resource::String::CorrelationArgumentDescription, ArgumentType::Standard, Argument::Visibility::Hidden }; + case Args::Type::ListDetails: + return Argument{ type, Resource::String::ListDetailsArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help, ExperimentalFeature::Feature::ListDetails }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/Commands/ListCommand.cpp b/src/AppInstallerCLICore/Commands/ListCommand.cpp index 93c5798498..334ddb56c9 100644 --- a/src/AppInstallerCLICore/Commands/ListCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ListCommand.cpp @@ -31,6 +31,7 @@ namespace AppInstaller::CLI Argument{ Execution::Args::Type::Upgrade, Resource::String::UpgradeArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }, Argument{ Execution::Args::Type::IncludeUnknown, Resource::String::IncludeUnknownInListArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::IncludePinned, Resource::String::IncludePinnedInListArgumentDescription, ArgumentType::Flag}, + Argument::ForType(Execution::Args::Type::ListDetails), }; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 3d701ac73f..3ce607cd3d 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -110,6 +110,7 @@ namespace AppInstaller::CLI::Execution // List Command Upgrade, // Used in List command to only show versions with upgrades + ListDetails, // Pin command GatedVersion, // Differs from Version in that this supports wildcards diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 7f07b0c142..06bda77852 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -432,6 +432,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Links); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ListDetailsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LocaleArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LocationArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(LogArgumentDescription); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index cada2d741f..9e3a0ee1b7 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -277,8 +277,20 @@ namespace AppInstaller::CLI::Workflow // Data shown on a line of a table displaying installed packages struct InstalledPackagesTableLine { - InstalledPackagesTableLine(Utility::LocIndString name, Utility::LocIndString id, Utility::LocIndString installedVersion, Utility::LocIndString availableVersion, Utility::LocIndString source) - : Name(name), Id(id), InstalledVersion(installedVersion), AvailableVersion(availableVersion), Source(source) {} + InstalledPackagesTableLine( + std::shared_ptr package, + std::shared_ptr installedVersion, + const Utility::LocIndString& availableVersion, + const Utility::LocIndString& source) + : Package(std::move(package)), InstalledPackageVersion(std::move(installedVersion)), AvailableVersion(availableVersion), Source(source) + { + Name = InstalledPackageVersion->GetProperty(PackageVersionProperty::Name); + Id = Package->GetProperty(PackageProperty::Id); + InstalledVersion = InstalledPackageVersion->GetProperty(PackageVersionProperty::Version); + } + + std::shared_ptr Package; + std::shared_ptr InstalledPackageVersion; Utility::LocIndString Name; Utility::LocIndString Id; @@ -287,7 +299,7 @@ namespace AppInstaller::CLI::Workflow Utility::LocIndString Source; }; - void OutputInstalledPackagesTable(Execution::Context& context, const std::vector& lines) + void OutputInstalledPackagesTable(Execution::Context& context, std::vector& lines) { Execution::TableOutput<5> table(context.Reporter, { @@ -298,7 +310,7 @@ namespace AppInstaller::CLI::Workflow Resource::String::SearchSource }); - for (const auto& line : lines) + for (auto& line : lines) { table.OutputLine({ line.Name, @@ -311,6 +323,32 @@ namespace AppInstaller::CLI::Workflow table.Complete(); } + + void OutputInstalledPackagesDetails(Execution::Context& context, std::vector& lines) + { + auto info = context.Reporter.Info(); + size_t packageIndex = 0; + for (auto& line : lines) + { + // TODO: Tons of data to go through in installed package version to see what to output + auto metadata = line.InstalledPackageVersion->GetMetadata(); + + info << '(' << ++packageIndex << '/' << lines.size() << ") "_liv << line.Name << '\n' + << " "_liv << "Install Location: "_liv << metadata[PackageVersionMetadata::InstalledLocation] << std::endl; + } + } + + void OutputInstalledPackages(Execution::Context& context, std::vector& lines) + { + if (context.Args.Contains(Execution::Args::Type::ListDetails)) + { + OutputInstalledPackagesDetails(context, lines); + } + else + { + OutputInstalledPackagesTable(context, lines); + } + } } bool WorkflowTask::operator==(const WorkflowTask& other) const @@ -1037,9 +1075,8 @@ namespace AppInstaller::CLI::Workflow // Add/Remove Programs entries. // TODO: De-duplicate this list, and only show (by default) one entry per matched package. InstalledPackagesTableLine line( - installedVersion->GetProperty(PackageVersionProperty::Name), - match.Package->GetProperty(PackageProperty::Id), - installedVersion->GetProperty(PackageVersionProperty::Version), + match.Package, + installedVersion, availableVersion, shouldShowSource ? sourceName : Utility::LocIndString() ); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 02388bd06f..49d5b370f8 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3458,4 +3458,8 @@ An unlocalized JSON fragment will follow on another line. Font package is already installed. - + + Show detailed information about packages + Providing this argument causes the CLI to output additional details about installed application packages. + + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index caeb32d0fe..cffc8acc7b 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Font: return userSettings.Get(); + case ExperimentalFeature::Feature::ListDetails: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -77,6 +79,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; case Feature::Font: return ExperimentalFeature{ "Font", "Font", "https://aka.ms/winget-settings", Feature::Font }; + case Feature::ListDetails: + return ExperimentalFeature{ "List Details", "listDetails", "https://aka.ms/winget-settings", Feature::ListDetails }; default: THROW_HR(E_UNEXPECTED); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 2dc097f548..33571c48aa 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,6 +25,7 @@ namespace AppInstaller::Settings DirectMSI = 0x1, Resume = 0x2, Font = 0x4, + ListDetails = 0x8, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 10b0c8f36b..e5e12c0b61 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFResume, EFFonts, + EFListDetails, // Telemetry TelemetryDisable, // Install behavior @@ -163,6 +164,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFListDetails, bool, bool, false, ".experimentalFeatures.listDetails"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index f220ec3c2c..b0f87fd625 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -267,6 +267,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(EFFonts) + WINGET_VALIDATE_PASS_THROUGH(EFListDetails) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) From 60049c846b3d0208de7963b316c146e632a4e225 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 11 Dec 2025 17:44:38 -0800 Subject: [PATCH 2/7] initial list of data produced --- src/AppInstallerCLICore/Argument.cpp | 8 +-- .../Workflows/WorkflowBase.cpp | 49 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 0f608a1280..d972dc9521 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -108,7 +108,7 @@ namespace AppInstaller::CLI case Execution::Args::Type::TargetVersion: return { type, "version"_liv, 'v', ArgTypeCategory::SinglePackageQuery, ArgTypeExclusiveSet::AllAndTargetVersion }; - //Source Command + // Source Command case Execution::Args::Type::SourceName: return { type, "name"_liv, 'n' }; case Execution::Args::Type::SourceType: @@ -122,13 +122,13 @@ namespace AppInstaller::CLI case Execution::Args::Type::SourceTrustLevel: return { type, "trust-level"_liv }; - //Hash Command + // Hash Command case Execution::Args::Type::HashFile: return { type, "file"_liv, 'f' }; case Execution::Args::Type::Msix: return { type, "msix"_liv, 'm' }; - //Validate Command + // Validate Command case Execution::Args::Type::ValidateManifest: return { type, "manifest"_liv }; case Execution::Args::Type::IgnoreWarnings: @@ -181,6 +181,8 @@ namespace AppInstaller::CLI // List command case Execution::Args::Type::Upgrade: return { type, "upgrade-available"_liv}; + case Execution::Args::Type::ListDetails: + return { type, "details"_liv }; // Pin command case Execution::Args::Type::GatedVersion: diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 9e3a0ee1b7..c84ebe0c25 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -324,17 +324,54 @@ namespace AppInstaller::CLI::Workflow table.Complete(); } + // Outputs every package "line" with many details, with a format similar to the `show` command. void OutputInstalledPackagesDetails(Execution::Context& context, std::vector& lines) { auto info = context.Reporter.Info(); size_t packageIndex = 0; for (auto& line : lines) { - // TODO: Tons of data to go through in installed package version to see what to output + // Identity header including package count indicator if multiple lines provided + if (lines.size() > 1) + { + info << '(' << ++packageIndex << '/' << lines.size() << ") "_liv; + } + + ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); + + // -- Package properties -- + // Id from Installed source data, like ARP\ProductCode + // Version actually provided already + // Channel if present + // Publisher + + // -- Package multi-properties [when present] -- + // PackageFamilyName, + // ProductCode, + // UpgradeCode, + + // -- Source -- + // Origin source if present + + // -- Metadata -- + // InstalledType, + // InstalledScope, + // InstalledLocation, + // InstalledLocale, + // TrackingWriteTime, ? + // InstalledArchitecture, + // PinnedState, ? + // UserIntentArchitecture, + // UserIntentLocale, + // NoModify, ? + // NoRepair, ? + + // -- Available package information -- + // Latest available version and ID per available source + auto metadata = line.InstalledPackageVersion->GetMetadata(); - info << '(' << ++packageIndex << '/' << lines.size() << ") "_liv << line.Name << '\n' - << " "_liv << "Install Location: "_liv << metadata[PackageVersionMetadata::InstalledLocation] << std::endl; + info << " "_liv << "Install Location: "_liv << metadata[PackageVersionMetadata::InstalledLocation] << std::endl; } } @@ -1098,7 +1135,7 @@ namespace AppInstaller::CLI::Workflow } } - OutputInstalledPackagesTable(context, lines); + OutputInstalledPackages(context, lines); if (lines.empty()) { @@ -1120,13 +1157,13 @@ namespace AppInstaller::CLI::Workflow if (!linesForExplicitUpgrade.empty()) { context.Reporter.Info() << std::endl << Resource::String::UpgradeAvailableForPinned << std::endl; - OutputInstalledPackagesTable(context, linesForExplicitUpgrade); + OutputInstalledPackages(context, linesForExplicitUpgrade); } if (!linesForPins.empty()) { context.Reporter.Info() << std::endl << Resource::String::UpgradeBlockedByPinCount(linesForPins.size()) << std::endl; - OutputInstalledPackagesTable(context, linesForPins); + OutputInstalledPackages(context, linesForPins); } if (m_onlyShowUpgrades) From 16221d420c93e9106a13efa5a66562f6abbf432b Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 12 Dec 2025 17:14:59 -0800 Subject: [PATCH 3/7] Refactoring to make show output functions reusable --- .../Workflows/ConfigurationFlow.cpp | 2 +- .../Workflows/ImportExportFlow.cpp | 2 +- .../Workflows/ShowFlow.cpp | 144 ++++++++++-------- src/AppInstallerCLICore/Workflows/ShowFlow.h | 13 +- .../Workflows/WorkflowBase.cpp | 2 + src/AppInstallerCLITests/Strings.cpp | 19 +-- src/AppInstallerCommonCore/Downloader.cpp | 2 +- .../AppInstallerStrings.cpp | 139 ++++++++++------- .../Public/AppInstallerLanguageUtilities.h | 106 +++++++++++++ .../Public/AppInstallerStrings.h | 11 +- .../Public/winget/LocIndependent.h | 1 + src/AppInstallerSharedLib/Versions.cpp | 2 +- 12 files changed, 310 insertions(+), 133 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 7c23fcab3c..20cc4ec22a 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -1417,7 +1417,7 @@ namespace AppInstaller::CLI::Workflow unit.Identifier(sourceNameWide + L'_' + packageIdWide); unit.Intent(ConfigurationUnitIntent::Apply); - auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ package.Id }); + auto description = Resource::String::ConfigureExportUnitInstallDescription(package.Id); ValueSet directives; directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index eaceb5e85c..f6d8bf8ec1 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -108,7 +108,7 @@ namespace AppInstaller::CLI::Workflow auto channel = installedPackageVersion->GetProperty(PackageVersionProperty::Channel); // Find an available version of this package to determine its source. - auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, Utility::LocIndView{ version }, Utility::LocIndView{ channel }, includeVersions); + auto availablePackageVersion = GetAvailableVersionForInstalledPackage(context, packageMatch.Package, version, channel, includeVersions); if (!availablePackageVersion) { // Report package not found and move to next package. diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp index ed4fa83261..4f3e2cae8c 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -11,90 +11,57 @@ using namespace AppInstaller::CLI; using namespace AppInstaller::Utility; using namespace AppInstaller::Utility::literals; -namespace { - - template - void ShowSingleLineField(Execution::OutputStream outputStream, AppInstaller::StringResource::StringId label, const String& value, bool indent = false) - { - if (value.empty()) - { - return; - } - if (indent) +namespace AppInstaller::CLI::Workflow +{ + namespace { + LocIndView GetIndentFor(size_t indentLevel) { - outputStream << " "_liv; + static constexpr std::array s_indents{ ""_liv, " "_liv, " "_liv, " "_liv }; + return s_indents.at(indentLevel); } - outputStream << Execution::ManifestInfoEmphasis << label << ' ' << value << std::endl; - } - template - void ShowMultiLineField(Execution::OutputStream outputStream, AppInstaller::StringResource::StringId label, const String& value) - { - if (value.empty()) + void ShowSingleLineField(Execution::OutputStream& outputStream, StringResource::StringId label, const Manifest::Manifest::string_t& value, bool indent = false) { - return; + Workflow::ShowSingleLineField(outputStream, label, LocIndView{ value }, indent ? 1 : 0); } - /* - We need to be able to find and replace within the string.However, we don't want to own the original string - Therefore, a copy is created here so we can manipulate it. The memory should be freed again once this method - returns and the string is no longer in scope. - */ - std::string shownValue = value; - bool isMultiLine = FindAndReplace(shownValue, "\n", "\n "); - outputStream << Execution::ManifestInfoEmphasis << label; - if (isMultiLine) - { - outputStream << std::endl << " "_liv << shownValue << std::endl; - } - else - { - outputStream << ' ' << shownValue << std::endl; - } - } - template - void ShowMultiValueField(Execution::OutputStream outputStream, AppInstaller::StringResource::StringId label, const Enumerable& values) - { - if (values.empty()) + void ShowMultiLineField(Execution::OutputStream& outputStream, StringResource::StringId label, const Manifest::Manifest::string_t& value) { - return; + Workflow::ShowMultiLineField(outputStream, label, LocIndView{ value }); } - outputStream << Execution::ManifestInfoEmphasis << label << std::endl; - for (const auto& value : values) + + void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, const std::vector& values) { - outputStream << " "_liv << value << std::endl; + Workflow::ShowMultiValueField(outputStream, label, Enumerable{ values, [](const Manifest::Manifest::string_t& s) { return LocIndString{ s }; } }); } - } - void ShowAgreements(Execution::OutputStream outputStream, const std::vector& agreements) { + void ShowAgreements(Execution::OutputStream& outputStream, const std::vector& agreements) { - if (agreements.empty()) { - return; - } + if (agreements.empty()) { + return; + } - outputStream << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelAgreements << std::endl; - for (const auto& agreement : agreements) { + outputStream << Execution::ManifestInfoEmphasis << Resource::String::ShowLabelAgreements << std::endl; + for (const auto& agreement : agreements) { - if (!agreement.Label.empty()) - { - outputStream << " "_liv << Execution::ManifestInfoEmphasis << agreement.Label << ": "_liv; - } + if (!agreement.Label.empty()) + { + outputStream << " "_liv << Execution::ManifestInfoEmphasis << agreement.Label << ": "_liv; + } - if (!agreement.AgreementText.empty()) - { - outputStream << agreement.AgreementText << std::endl; - } + if (!agreement.AgreementText.empty()) + { + outputStream << agreement.AgreementText << std::endl; + } - if (!agreement.AgreementUrl.empty()) - { - outputStream << agreement.AgreementUrl << std::endl; + if (!agreement.AgreementUrl.empty()) + { + outputStream << agreement.AgreementUrl << std::endl; + } } } } -} -namespace AppInstaller::CLI::Workflow -{ void ShowAgreementsInfo(Execution::Context& context) { const auto& manifest = context.Get(); @@ -264,4 +231,53 @@ namespace AppInstaller::CLI::Workflow GetManifestFromPackage(m_considerPins); } } + + void ShowSingleLineField(Execution::OutputStream& outputStream, StringResource::StringId label, LocIndView value, size_t indentLevel) + { + if (value.empty()) + { + return; + } + + outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << ' ' << value << '\n'; + } + + void ShowMultiLineField(Execution::OutputStream& outputStream, StringResource::StringId label, LocIndView value, size_t indentLevel) + { + if (value.empty()) + { + return; + } + + auto lines = Split(value, '\n'); + + outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label; + + if (lines.size() > 1) + { + for (const auto& line : lines) + { + outputStream << '\n' << GetIndentFor(indentLevel + 1) << line << '\n'; + } + } + else + { + outputStream << ' ' << value << '\n'; + } + } + + void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, Enumerable values, size_t indentLevel) + { + if (values.AtEnd()) + { + return; + } + + outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << '\n'; + + do + { + outputStream << GetIndentFor(indentLevel + 1) << values.Current() << '\n'; + } while (values.Next()); + } } diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.h b/src/AppInstallerCLICore/Workflows/ShowFlow.h index 220ea223ff..bda9ddd04f 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.h +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.h @@ -2,6 +2,9 @@ // Licensed under the MIT License. #pragma once #include "ExecutionContext.h" +#include +#include +#include namespace AppInstaller::CLI::Workflow { @@ -48,4 +51,12 @@ namespace AppInstaller::CLI::Workflow private: bool m_considerPins; }; -} \ No newline at end of file + + // Reusable helpers for `show` style line output + + void ShowSingleLineField(Execution::OutputStream& outputStream, StringResource::StringId label, Utility::LocIndView value, size_t indentLevel = 0); + + void ShowMultiLineField(Execution::OutputStream& outputStream, StringResource::StringId label, Utility::LocIndView value, size_t indentLevel = 0); + + void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, Enumerable values, size_t indentLevel = 0); +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index c84ebe0c25..f707ea3d8d 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -339,6 +339,8 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); + + // -- Package properties -- // Id from Installed source data, like ARP\ProductCode // Version actually provided already diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index 06884657d0..dfe4e97f76 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -6,6 +6,7 @@ #include #include +using namespace std::string_literals; using namespace std::string_view_literals; using namespace AppInstaller::Utility; using namespace AppInstaller::Utility::literals; @@ -118,8 +119,8 @@ TEST_CASE("Trim", "[strings]") REQUIRE(Trim(str.assign(" Multiple words")) == "Multiple words"); REQUIRE(Trim(str.assign("Much after is taken \f\n\r\t\v\v\t\r\n\f ")) == "Much after is taken"); - REQUIRE(Trim(L" Test") == L"Test"); - REQUIRE(Trim(L" ") == L""); + REQUIRE(Trim(L" Test"sv) == L"Test"); + REQUIRE(Trim(L" "sv) == L""); } TEST_CASE("CaseInsensitiveStartsWith", "[strings]") @@ -280,38 +281,38 @@ TEST_CASE("SplitIntoLines", "[strings]") TEST_CASE("SplitWithSeparator", "[strings]") { - std::vector test1 = Split("first;second;third", ';'); + std::vector test1 = Split("first;second;third"s, ';'); REQUIRE(test1.size() == 3); REQUIRE(test1[0] == "first"); REQUIRE(test1[1] == "second"); REQUIRE(test1[2] == "third"); - std::vector test2 = Split("two spaces", ' '); + std::vector test2 = Split("two spaces"s, ' '); REQUIRE(test2.size() == 3); REQUIRE(test2[0] == "two"); REQUIRE(test2[1] == ""); REQUIRE(test2[2] == "spaces"); - std::vector test3 = Split("test", '.'); + std::vector test3 = Split("test"s, '.'); REQUIRE(test3.size() == 1); REQUIRE(test3[0] == "test"); - std::vector test4 = Split(" trim | spaces ", '|', true); + std::vector test4 = Split(" trim | spaces "s, '|', true); REQUIRE(test4.size() == 2); REQUIRE(test4[0] == "trim"); REQUIRE(test4[1] == "spaces"); - std::vector test5 = Split(L" trim /spaces / ", '/', true); + std::vector test5 = Split(L" trim /spaces / "sv, '/', true); REQUIRE(test5.size() == 3); REQUIRE(test5[0] == L"trim"); REQUIRE(test5[1] == L"spaces"); REQUIRE(test5[2] == L""); - std::vector test6 = Split(L" ", '/', true); + std::vector test6 = Split(L" "sv, '/', true); REQUIRE(test6.size() == 1); REQUIRE(test6[0] == L""); - std::vector test7 = Split("", ';'); + std::vector test7 = Split(""s, ';'); REQUIRE(test7.size() == 1); REQUIRE(test7[0] == ""); } diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index 37f00d9c44..a073f59861 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -649,7 +649,7 @@ namespace AppInstaller::Utility } else if (StartsWith(lowerDirective, s_MaxAge)) { - std::vector parts = Utility::Split(lowerDirective, L'=', true); + std::vector parts = Utility::SplitView(lowerDirective, L'=', true); if (parts.size() == 2) { try diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 83e9971e6f..23790b80a5 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -98,6 +98,43 @@ namespace AppInstaller::Utility int32_t m_currentBrk = 0; }; + template + StringType& TrimTemplate(StringType& input, std::basic_string_view spaceChars) + { + if (!input.empty()) + { + size_t begin = input.find_first_not_of(spaceChars); + size_t end = input.find_last_not_of(spaceChars); + + if (begin == StringType::npos || end == StringType::npos) + { + input = {}; + } + else if (begin != 0 || end != input.length() - 1) + { + input = input.substr(begin, (end - begin) + 1); + } + } + + return input; + } + + template + StringType TrimCopyTemplate(const StringType& input) + { + StringType result = input; + Utility::Trim(result); + return result; + } + + template + StringType TrimMoveTemplate(StringType&& input) + { + StringType result = std::move(input); + Utility::Trim(result); + return result; + } + template std::vector SplitTemplate(const StringType& input, typename StringType::value_type separator, bool trim) { @@ -557,76 +594,52 @@ namespace AppInstaller::Utility std::string& Trim(std::string& str) { - if (!str.empty()) - { - size_t begin = str.find_first_not_of(s_SpaceChars); - size_t end = str.find_last_not_of(s_SpaceChars); - - if (begin == std::string_view::npos || end == std::string_view::npos) - { - str.clear(); - } - else if (begin != 0 || end != str.length() - 1) - { - str = str.substr(begin, (end - begin) + 1); - } - } + return TrimTemplate(str, s_SpaceChars); + } - return str; + std::string Trim(const std::string& str) + { + return TrimCopyTemplate(str); } - std::wstring& Trim(std::wstring& str) + std::string Trim(std::string&& str) { - if (!str.empty()) - { - size_t begin = str.find_first_not_of(s_WideSpaceChars); - size_t end = str.find_last_not_of(s_WideSpaceChars); + return TrimMoveTemplate(std::move(str)); + } - if (begin == std::string_view::npos || end == std::string_view::npos) - { - str.clear(); - } - else if (begin != 0 || end != str.length() - 1) - { - str = str.substr(begin, (end - begin) + 1); - } - } + std::string_view& Trim(std::string_view& str) + { + return TrimTemplate(str, s_SpaceChars); + } - return str; + std::string_view Trim(std::string_view&& str) + { + return TrimCopyTemplate(str); } - std::wstring_view& Trim(std::wstring_view& str) + std::wstring& Trim(std::wstring& str) { - if (!str.empty()) - { - size_t begin = str.find_first_not_of(s_WideSpaceChars); - size_t end = str.find_last_not_of(s_WideSpaceChars); + return TrimTemplate(str, s_WideSpaceChars); + } - if (begin == std::string_view::npos || end == std::string_view::npos) - { - str = {}; - } - else if (begin != 0 || end != str.length() - 1) - { - str = str.substr(begin, (end - begin) + 1); - } - } + std::wstring Trim(const std::wstring& str) + { + return TrimCopyTemplate(str); + } - return str; + std::wstring Trim(std::wstring&& str) + { + return TrimMoveTemplate(std::move(str)); } - std::wstring_view Trim(std::wstring_view&& str) + std::wstring_view& Trim(std::wstring_view& str) { - std::wstring_view result = std::move(str); - Utility::Trim(result); - return result; + return TrimTemplate(str, s_WideSpaceChars); } - std::string Trim(std::string&& str) + std::wstring_view Trim(std::wstring_view&& str) { - std::string result = std::move(str); - Utility::Trim(result); - return result; + return TrimCopyTemplate(str); } std::string ReadEntireStream(std::istream& stream) @@ -929,6 +942,26 @@ namespace AppInstaller::Utility return SplitTemplate(input, separator, trim); } + std::vector SplitView(const std::string& input, char separator, bool trim) + { + return SplitTemplate(static_cast(input), separator, trim); + } + + std::vector Split(std::string_view input, char separator, bool trim) + { + return SplitTemplate(input, separator, trim); + } + + std::vector Split(const std::wstring& input, wchar_t separator, bool trim) + { + return SplitTemplate(input, separator, trim); + } + + std::vector SplitView(const std::wstring& input, wchar_t separator, bool trim) + { + return SplitTemplate(static_cast(input), separator, trim); + } + std::vector Split(std::wstring_view input, wchar_t separator, bool trim) { return SplitTemplate(input, separator, trim); diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h index 9b958342c1..c49552d6c3 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include #include #include +#include +#include #include #include #include @@ -208,6 +211,109 @@ namespace AppInstaller return result; } + + // Creates a type erased wrapper to enable enumeration of arbitrary containers based on value type. + template + struct Enumerable + { + struct IEnumerableTypeErased + { + virtual ~IEnumerableTypeErased() = default; + + virtual T& Current() = 0; + virtual bool Next() = 0; + virtual bool AtEnd() = 0; + }; + + template + struct EnumerableTypeErased : public IEnumerableTypeErased + { + using ItrType = decltype(std::begin(std::declval())); + + ItrType m_begin; + ItrType m_end; + + EnumerableTypeErased(U&& container) : + m_begin(std::begin(container)), + m_end(std::end(container)) + { + } + + T& Current() override + { + THROW_HR_IF(E_BOUNDS, AtEnd()); + return *m_begin; + } + + bool Next() override + { + THROW_HR_IF(E_BOUNDS, AtEnd()); + ++m_begin; + return !AtEnd(); + } + + bool AtEnd() override + { + return m_begin == m_end; + } + }; + + template + struct EnumerableTypeErasedWithConversion : public IEnumerableTypeErased + { + using ItrType = decltype(std::begin(std::declval())); + + ItrType m_begin; + ItrType m_end; + F m_function; + std::optional m_currentConverted; + + EnumerableTypeErasedWithConversion(U&& container, F&& function) : + m_begin(std::begin(container)), + m_end(std::end(container)), + m_function(std::forward(function)) + { + } + + T& Current() override + { + THROW_HR_IF(E_BOUNDS, AtEnd()); + m_currentConverted = m_function(*m_begin); + return m_currentConverted.value(); + } + + bool Next() override + { + THROW_HR_IF(E_BOUNDS, AtEnd()); + ++m_begin; + return !AtEnd(); + } + + bool AtEnd() override + { + return m_begin == m_end; + } + }; + + std::unique_ptr m_typeErased; + + template + Enumerable(U&& container) + { + m_typeErased = std::make_unique>(std::forward(container)); + } + + template + Enumerable(U&& container, F&& function) + { + static_assert(std::is_same_v()(*std::begin(std::declval())))>>, "The function must convert the container's value type to the desired output type."); + m_typeErased = std::make_unique>(std::forward(container), std::forward(function)); + } + + T& Current() { return m_typeErased->Current(); } + bool Next() { return m_typeErased->Next(); } + bool AtEnd() { return m_typeErased->AtEnd(); } + }; } // Enable enums to be output generically (as their integral value). diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 5ead01371e..f535c89240 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -176,12 +176,15 @@ namespace AppInstaller::Utility // Removes whitespace from the beginning and end of the string. std::string& Trim(std::string& str); + std::string Trim(const std::string& str); std::string Trim(std::string&& str); + std::string_view& Trim(std::string_view& str); + std::string_view Trim(std::string_view&& str); // Removes whitespace from the beginning and end of the string. std::wstring& Trim(std::wstring& str); - - // Removes whitespace from the beginning and end of the string. + std::wstring Trim(const std::wstring& str); + std::wstring Trim(std::wstring&& str); std::wstring_view& Trim(std::wstring_view& str); std::wstring_view Trim(std::wstring_view&& str); @@ -273,8 +276,12 @@ namespace AppInstaller::Utility // Splits the string using the provided separator. Entries can also be trimmed. std::vector Split(const std::string& input, char separator, bool trim = false); + std::vector SplitView(const std::string& input, char separator, bool trim = false); + std::vector Split(std::string_view input, char separator, bool trim = false); // Splits the string using the provided separator. Entries can also be trimmed. + std::vector Split(const std::wstring& input, wchar_t separator, bool trim = false); + std::vector SplitView(const std::wstring& input, wchar_t separator, bool trim = false); std::vector Split(std::wstring_view input, wchar_t separator, bool trim = false); // Format an input string by replacing placeholders {index} with provided values at corresponding indices. diff --git a/src/AppInstallerSharedLib/Public/winget/LocIndependent.h b/src/AppInstallerSharedLib/Public/winget/LocIndependent.h index abdec52e21..d0288d6dbf 100644 --- a/src/AppInstallerSharedLib/Public/winget/LocIndependent.h +++ b/src/AppInstallerSharedLib/Public/winget/LocIndependent.h @@ -40,6 +40,7 @@ namespace AppInstaller::Utility operator const std::string& () const { return m_value; } operator std::string_view() const { return m_value; } + operator LocIndView() const { return LocIndView{ static_cast(m_value) }; } const std::string* operator->() const { return &m_value; } diff --git a/src/AppInstallerSharedLib/Versions.cpp b/src/AppInstallerSharedLib/Versions.cpp index 975f3367a2..0d9d5d5732 100644 --- a/src/AppInstallerSharedLib/Versions.cpp +++ b/src/AppInstallerSharedLib/Versions.cpp @@ -283,7 +283,7 @@ namespace AppInstaller::Utility Version::Part::Part(const std::string& part) { - std::string interimPart = Utility::Trim(part.c_str()); + std::string interimPart = Utility::Trim(part); const char* begin = interimPart.c_str(); char* end = nullptr; errno = 0; From c5556351a203aa994d5735a8a8d45ab4c7c102e5 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 16 Dec 2025 09:52:30 -0800 Subject: [PATCH 4/7] Many fields --- .../Workflows/ShowFlow.cpp | 44 +++----- src/AppInstallerCLICore/Workflows/ShowFlow.h | 20 +++- .../Workflows/WorkflowBase.cpp | 100 +++++++++++------ .../Public/AppInstallerLanguageUtilities.h | 103 ------------------ 4 files changed, 101 insertions(+), 166 deletions(-) diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp index 4f3e2cae8c..ca3e01d421 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -13,13 +13,8 @@ using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI::Workflow { - namespace { - LocIndView GetIndentFor(size_t indentLevel) - { - static constexpr std::array s_indents{ ""_liv, " "_liv, " "_liv, " "_liv }; - return s_indents.at(indentLevel); - } - + namespace + { void ShowSingleLineField(Execution::OutputStream& outputStream, StringResource::StringId label, const Manifest::Manifest::string_t& value, bool indent = false) { Workflow::ShowSingleLineField(outputStream, label, LocIndView{ value }, indent ? 1 : 0); @@ -30,11 +25,6 @@ namespace AppInstaller::CLI::Workflow Workflow::ShowMultiLineField(outputStream, label, LocIndView{ value }); } - void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, const std::vector& values) - { - Workflow::ShowMultiValueField(outputStream, label, Enumerable{ values, [](const Manifest::Manifest::string_t& s) { return LocIndString{ s }; } }); - } - void ShowAgreements(Execution::OutputStream& outputStream, const std::vector& agreements) { if (agreements.empty()) { @@ -62,6 +52,15 @@ namespace AppInstaller::CLI::Workflow } } + namespace details + { + LocIndView GetIndentFor(size_t indentLevel) + { + static constexpr std::array s_indents{ ""_liv, " "_liv, " "_liv, " "_liv }; + return s_indents.at(indentLevel); + } + } + void ShowAgreementsInfo(Execution::Context& context) { const auto& manifest = context.Get(); @@ -239,7 +238,7 @@ namespace AppInstaller::CLI::Workflow return; } - outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << ' ' << value << '\n'; + outputStream << details::GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << ' ' << value << '\n'; } void ShowMultiLineField(Execution::OutputStream& outputStream, StringResource::StringId label, LocIndView value, size_t indentLevel) @@ -251,13 +250,13 @@ namespace AppInstaller::CLI::Workflow auto lines = Split(value, '\n'); - outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label; + outputStream << details::GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label; if (lines.size() > 1) { for (const auto& line : lines) { - outputStream << '\n' << GetIndentFor(indentLevel + 1) << line << '\n'; + outputStream << '\n' << details::GetIndentFor(indentLevel + 1) << line << '\n'; } } else @@ -265,19 +264,4 @@ namespace AppInstaller::CLI::Workflow outputStream << ' ' << value << '\n'; } } - - void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, Enumerable values, size_t indentLevel) - { - if (values.AtEnd()) - { - return; - } - - outputStream << GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << '\n'; - - do - { - outputStream << GetIndentFor(indentLevel + 1) << values.Current() << '\n'; - } while (values.Next()); - } } diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.h b/src/AppInstallerCLICore/Workflows/ShowFlow.h index bda9ddd04f..42947d130d 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.h +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.h @@ -53,10 +53,28 @@ namespace AppInstaller::CLI::Workflow }; // Reusable helpers for `show` style line output + namespace details + { + Utility::LocIndView GetIndentFor(size_t i); + } void ShowSingleLineField(Execution::OutputStream& outputStream, StringResource::StringId label, Utility::LocIndView value, size_t indentLevel = 0); void ShowMultiLineField(Execution::OutputStream& outputStream, StringResource::StringId label, Utility::LocIndView value, size_t indentLevel = 0); - void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, Enumerable values, size_t indentLevel = 0); + template + void ShowMultiValueField(Execution::OutputStream& outputStream, StringResource::StringId label, const Container& values, size_t indentLevel = 0) + { + if (values.empty()) + { + return; + } + + outputStream << details::GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << '\n'; + + for (const auto& value : values) + { + outputStream << details::GetIndentFor(indentLevel + 1) << value << '\n'; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index f707ea3d8d..c2c6c302f0 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -5,6 +5,7 @@ #include "ExecutionContext.h" #include #include "PromptFlow.h" +#include "ShowFlow.h" #include "Sixel.h" #include "TableOutput.h" #include @@ -324,56 +325,91 @@ namespace AppInstaller::CLI::Workflow table.Complete(); } + void ShowMetadataField( + Execution::OutputStream& outputStream, + StringResource::StringId label, + const IPackageVersion::Metadata& metadata, + PackageVersionMetadata field) + { + auto itr = metadata.find(field); + if (itr != metadata.end()) + { + // TODO: Maybe not loc ind? + ShowSingleLineField(outputStream, label, Utility::LocIndView{ itr->second }); + } + } + // Outputs every package "line" with many details, with a format similar to the `show` command. void OutputInstalledPackagesDetails(Execution::Context& context, std::vector& lines) { auto info = context.Reporter.Info(); size_t packageIndex = 0; - for (auto& line : lines) + size_t totalLines = lines.size(); + for (const auto& line : lines) { // Identity header including package count indicator if multiple lines provided - if (lines.size() > 1) + if (totalLines > 1) { - info << '(' << ++packageIndex << '/' << lines.size() << ") "_liv; + info << '(' << ++packageIndex << '/' << totalLines << ") "_liv; } ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); +#define TEMP_STRING(_unused_) Resource::String::SourceListName + ShowSingleLineField(info, Resource::String::ShowLabelVersion, line.InstalledVersion); + ShowSingleLineField(info, Resource::String::ShowChannel, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Channel)); + ShowSingleLineField(info, Resource::String::ShowLabelPublisher, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Publisher)); + auto localIdentifier = line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Id); + if (line.Id != localIdentifier) + { + ShowSingleLineField(info, TEMP_STRING(Resource::String::ShowListLocalIdentifier), localIdentifier); + } - // -- Package properties -- - // Id from Installed source data, like ARP\ProductCode - // Version actually provided already - // Channel if present - // Publisher - - // -- Package multi-properties [when present] -- - // PackageFamilyName, - // ProductCode, - // UpgradeCode, - - // -- Source -- - // Origin source if present + ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListPackageFamilyName), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName)); + ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListProductCode), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode)); + ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListUpgradeCode), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode)); - // -- Metadata -- - // InstalledType, - // InstalledScope, - // InstalledLocation, - // InstalledLocale, - // TrackingWriteTime, ? - // InstalledArchitecture, - // PinnedState, ? - // UserIntentArchitecture, - // UserIntentLocale, - // NoModify, ? - // NoRepair, ? + auto metadata = line.InstalledPackageVersion->GetMetadata(); - // -- Available package information -- - // Latest available version and ID per available source + ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::InstalledType); + ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledScope), metadata, PackageVersionMetadata::InstalledScope); + ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledArchitecture), metadata, PackageVersionMetadata::InstalledArchitecture); + ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListUserIntentArchitecture), metadata, PackageVersionMetadata::UserIntentArchitecture); + ShowMetadataField(info, Resource::String::ShowLabelInstallerLocale, metadata, PackageVersionMetadata::InstalledLocale); + ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListUserIntentLocale), metadata, PackageVersionMetadata::UserIntentLocale); + ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledLocation), metadata, PackageVersionMetadata::InstalledLocation); + + // TODO: Check on these + ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::TrackingWriteTime); + ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::PinnedState); + ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::NoModify); + ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::NoRepair); + + auto source = line.InstalledPackageVersion->GetSource(); + if (source.ContainsAvailablePackages()) + { + ShowSingleLineField(info, TEMP_STRING(Resource::String::ShowListInstalledSource), Utility::LocIndView{ source.GetDetails().Name }); + } - auto metadata = line.InstalledPackageVersion->GetMetadata(); + Utility::Version currentVersion{ line.InstalledVersion }; + bool hasUpgradeVersion = false; + for (const auto& available : line.Package->GetAvailable()) + { + auto latestAvailable = available->GetLatestVersion(); + auto availableVersion = latestAvailable->GetProperty(PackageVersionProperty::Version); + if (Utility::Version{ availableVersion } > currentVersion) + { + if (!hasUpgradeVersion) + { + hasUpgradeVersion = true; + info << details::GetIndentFor(0) << Execution::ManifestInfoEmphasis << TEMP_STRING(Resource::String::ShowListAvailableUpgrades) << '\n'; + } - info << " "_liv << "Install Location: "_liv << metadata[PackageVersionMetadata::InstalledLocation] << std::endl; + info << details::GetIndentFor(1) << Utility::LocIndView{ available->GetSource().GetDetails().Name } << + " ["_liv << availableVersion << "]\n"_liv; + } + } } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h index c49552d6c3..187f76aaa3 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLanguageUtilities.h @@ -211,109 +211,6 @@ namespace AppInstaller return result; } - - // Creates a type erased wrapper to enable enumeration of arbitrary containers based on value type. - template - struct Enumerable - { - struct IEnumerableTypeErased - { - virtual ~IEnumerableTypeErased() = default; - - virtual T& Current() = 0; - virtual bool Next() = 0; - virtual bool AtEnd() = 0; - }; - - template - struct EnumerableTypeErased : public IEnumerableTypeErased - { - using ItrType = decltype(std::begin(std::declval())); - - ItrType m_begin; - ItrType m_end; - - EnumerableTypeErased(U&& container) : - m_begin(std::begin(container)), - m_end(std::end(container)) - { - } - - T& Current() override - { - THROW_HR_IF(E_BOUNDS, AtEnd()); - return *m_begin; - } - - bool Next() override - { - THROW_HR_IF(E_BOUNDS, AtEnd()); - ++m_begin; - return !AtEnd(); - } - - bool AtEnd() override - { - return m_begin == m_end; - } - }; - - template - struct EnumerableTypeErasedWithConversion : public IEnumerableTypeErased - { - using ItrType = decltype(std::begin(std::declval())); - - ItrType m_begin; - ItrType m_end; - F m_function; - std::optional m_currentConverted; - - EnumerableTypeErasedWithConversion(U&& container, F&& function) : - m_begin(std::begin(container)), - m_end(std::end(container)), - m_function(std::forward(function)) - { - } - - T& Current() override - { - THROW_HR_IF(E_BOUNDS, AtEnd()); - m_currentConverted = m_function(*m_begin); - return m_currentConverted.value(); - } - - bool Next() override - { - THROW_HR_IF(E_BOUNDS, AtEnd()); - ++m_begin; - return !AtEnd(); - } - - bool AtEnd() override - { - return m_begin == m_end; - } - }; - - std::unique_ptr m_typeErased; - - template - Enumerable(U&& container) - { - m_typeErased = std::make_unique>(std::forward(container)); - } - - template - Enumerable(U&& container, F&& function) - { - static_assert(std::is_same_v()(*std::begin(std::declval())))>>, "The function must convert the container's value type to the desired output type."); - m_typeErased = std::make_unique>(std::forward(container), std::forward(function)); - } - - T& Current() { return m_typeErased->Current(); } - bool Next() { return m_typeErased->Next(); } - bool AtEnd() { return m_typeErased->AtEnd(); } - }; } // Enable enums to be output generically (as their integral value). From 645eaf9da7e97aeee42c66d1417c2bcf9519fb99 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 17 Dec 2025 17:56:02 -0800 Subject: [PATCH 5/7] Mostly done --- src/AppInstallerCLICore/Resources.h | 11 +++++ src/AppInstallerCLICore/Workflows/ShowFlow.h | 6 ++- .../Workflows/WorkflowBase.cpp | 35 ++++++--------- .../Shared/Strings/en-us/winget.resw | 44 +++++++++++++++++++ 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 06bda77852..d120f2e360 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -636,6 +636,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAgreements); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelAuthor); + WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelChannel); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelCopyright); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelCopyrightUrl); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelDependencies); @@ -667,6 +668,16 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelVersion); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelWindowsFeaturesDependencies); WINGET_DEFINE_RESOURCE_STRINGID(ShowLabelWindowsLibrariesDependencies); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListAvailableUpgrades); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledArchitecture); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledLocale); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledLocation); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledScope); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledSource); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListLocalIdentifier); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListPackageFamilyName); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListProductCode); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListUpgradeCode); WINGET_DEFINE_RESOURCE_STRINGID(ShowVersion); WINGET_DEFINE_RESOURCE_STRINGID(SilentArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SingleCharAfterDashError); diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.h b/src/AppInstallerCLICore/Workflows/ShowFlow.h index 42947d130d..182f06d3ff 100644 --- a/src/AppInstallerCLICore/Workflows/ShowFlow.h +++ b/src/AppInstallerCLICore/Workflows/ShowFlow.h @@ -70,11 +70,13 @@ namespace AppInstaller::CLI::Workflow return; } - outputStream << details::GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label << '\n'; + bool isMultiItem = values.size() > 1; + outputStream << details::GetIndentFor(indentLevel) << Execution::ManifestInfoEmphasis << label; + outputStream << (isMultiItem ? '\n' : ' '); for (const auto& value : values) { - outputStream << details::GetIndentFor(indentLevel + 1) << value << '\n'; + outputStream << details::GetIndentFor(isMultiItem ? indentLevel + 1 : 0) << value << '\n'; } } } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index c2c6c302f0..a1dd269062 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -334,7 +334,6 @@ namespace AppInstaller::CLI::Workflow auto itr = metadata.find(field); if (itr != metadata.end()) { - // TODO: Maybe not loc ind? ShowSingleLineField(outputStream, label, Utility::LocIndView{ itr->second }); } } @@ -355,41 +354,31 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); -#define TEMP_STRING(_unused_) Resource::String::SourceListName - ShowSingleLineField(info, Resource::String::ShowLabelVersion, line.InstalledVersion); - ShowSingleLineField(info, Resource::String::ShowChannel, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Channel)); + ShowSingleLineField(info, Resource::String::ShowLabelChannel, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Channel)); ShowSingleLineField(info, Resource::String::ShowLabelPublisher, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Publisher)); auto localIdentifier = line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Id); if (line.Id != localIdentifier) { - ShowSingleLineField(info, TEMP_STRING(Resource::String::ShowListLocalIdentifier), localIdentifier); + ShowSingleLineField(info, Resource::String::ShowListLocalIdentifier, localIdentifier); } - ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListPackageFamilyName), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName)); - ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListProductCode), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode)); - ShowMultiValueField(info, TEMP_STRING(Resource::String::ShowListUpgradeCode), line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode)); + ShowMultiValueField(info, Resource::String::ShowListPackageFamilyName, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName)); + ShowMultiValueField(info, Resource::String::ShowListProductCode, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode)); + ShowMultiValueField(info, Resource::String::ShowListUpgradeCode, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode)); auto metadata = line.InstalledPackageVersion->GetMetadata(); ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::InstalledType); - ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledScope), metadata, PackageVersionMetadata::InstalledScope); - ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledArchitecture), metadata, PackageVersionMetadata::InstalledArchitecture); - ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListUserIntentArchitecture), metadata, PackageVersionMetadata::UserIntentArchitecture); - ShowMetadataField(info, Resource::String::ShowLabelInstallerLocale, metadata, PackageVersionMetadata::InstalledLocale); - ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListUserIntentLocale), metadata, PackageVersionMetadata::UserIntentLocale); - ShowMetadataField(info, TEMP_STRING(Resource::String::ShowListInstalledLocation), metadata, PackageVersionMetadata::InstalledLocation); - - // TODO: Check on these - ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::TrackingWriteTime); - ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::PinnedState); - ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::NoModify); - ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::NoRepair); + ShowMetadataField(info, Resource::String::ShowListInstalledScope, metadata, PackageVersionMetadata::InstalledScope); + ShowMetadataField(info, Resource::String::ShowListInstalledArchitecture, metadata, PackageVersionMetadata::InstalledArchitecture); + ShowMetadataField(info, Resource::String::ShowListInstalledLocale, metadata, PackageVersionMetadata::InstalledLocale); + ShowMetadataField(info, Resource::String::ShowListInstalledLocation, metadata, PackageVersionMetadata::InstalledLocation); auto source = line.InstalledPackageVersion->GetSource(); if (source.ContainsAvailablePackages()) { - ShowSingleLineField(info, TEMP_STRING(Resource::String::ShowListInstalledSource), Utility::LocIndView{ source.GetDetails().Name }); + ShowSingleLineField(info, Resource::String::ShowListInstalledSource, Utility::LocIndView{ source.GetDetails().Name }); } Utility::Version currentVersion{ line.InstalledVersion }; @@ -403,13 +392,15 @@ namespace AppInstaller::CLI::Workflow if (!hasUpgradeVersion) { hasUpgradeVersion = true; - info << details::GetIndentFor(0) << Execution::ManifestInfoEmphasis << TEMP_STRING(Resource::String::ShowListAvailableUpgrades) << '\n'; + info << details::GetIndentFor(0) << Execution::ManifestInfoEmphasis << Resource::String::ShowListAvailableUpgrades << '\n'; } info << details::GetIndentFor(1) << Utility::LocIndView{ available->GetSource().GetDetails().Name } << " ["_liv << availableVersion << "]\n"_liv; } } + + // FUTURE: We could also pull data from the tracking database to show some things that we store there specifically. } } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 49d5b370f8..bbe612f82a 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3462,4 +3462,48 @@ An unlocalized JSON fragment will follow on another line. Show detailed information about packages Providing this argument causes the CLI to output additional details about installed application packages. + + Channel: + Precedes a string value that names the delivery channel for the software package (ex. stable, beta). + + + Local Identifier: + Precedes a value that is the unique identifier for the installed package on the local system. + + + Package Family Name: + Precedes a value that is the APPX/MSIX package family name of the installed package. + + + Product Code: + Precedes a value that is the Add/Remove Programs identifier in the registry. This is also the Product Code value as defined in MSI installers. + + + Upgrade Code: + Precedes a value that is the MSI Upgrade Code for the installed package. + + + Installed Scope: + Precedes a value that is the scope of the installation of the package (ex. user, machine). + + + Installed Architecture: + Precedes a value that is the installed architecture of the package (ex. x86, x64, ARM64). + + + Installed Locale: + Precedes a value that is the locale of the installed package. + + + Installed Location: + Precedes a value that is the directory path to the installed package. + + + Origin Source: + Precedes a value that names the package source where the installed package originated from. + + + Available Upgrades: + Precedes a list of package upgrades available for the installed package. + \ No newline at end of file From 308a2e05a080f7266d83a7568972bb77af2de6f3 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 18 Dec 2025 17:32:47 -0800 Subject: [PATCH 6/7] Feedback and local icon extraction --- src/AppInstallerCLICore/Resources.h | 1 + src/AppInstallerCLICore/Sixel.cpp | 13 ++- src/AppInstallerCLICore/Sixel.h | 6 ++ .../Workflows/WorkflowBase.cpp | 88 +++++++++++++++---- .../Shared/Strings/en-us/winget.resw | 4 + 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index d120f2e360..e4b8926c0a 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -674,6 +674,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledLocation); WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledScope); WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstalledSource); + WINGET_DEFINE_RESOURCE_STRINGID(ShowListInstallerCategory); WINGET_DEFINE_RESOURCE_STRINGID(ShowListLocalIdentifier); WINGET_DEFINE_RESOURCE_STRINGID(ShowListPackageFamilyName); WINGET_DEFINE_RESOURCE_STRINGID(ShowListProductCode); diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index bae189981c..7bd84c1480 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -432,15 +432,18 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - ImageSource::ImageSource(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) + ImageSource::ImageSource(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) : + ImageSource(Utility::ReadEntireStreamAsByteArray(imageStream), imageEncoding) + { + } + + ImageSource::ImageSource(const std::vector& imageBytes, Manifest::IconFileTypeEnum imageEncoding) { m_factory = anon::CreateFactory(); wil::com_ptr stream; THROW_IF_FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &stream)); - auto imageBytes = Utility::ReadEntireStreamAsByteArray(imageStream); - ULONG written = 0; THROW_IF_FAILED(stream->Write(imageBytes.data(), static_cast(imageBytes.size()), &written)); THROW_IF_FAILED(stream->Seek({}, STREAM_SEEK_SET, nullptr)); @@ -637,6 +640,10 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel m_imageSource(imageStream, imageEncoding) {} + Image::Image(const std::vector& imageBytes, Manifest::IconFileTypeEnum imageEncoding) : + m_imageSource(imageBytes, imageEncoding) + {} + Image& Image::AspectRatio(Sixel::AspectRatio aspectRatio) { m_renderControls.AspectRatio = aspectRatio; diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 737dde3830..308353c608 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -145,6 +145,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Create an image source from a stream. ImageSource(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding); + // Create an image source from bytes. + ImageSource(const std::vector& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + // Resize the image to the given width and height, factoring in the target aspect ratio for rendering. // If stretchToFill is true, the resulting image will be both the given width and height. // If false, the resulting image will be at most the given width or height while preserving the aspect ratio. @@ -221,6 +224,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Create an image from a stream. Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding); + // Create an image from bytes. + Image(const std::vector& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + // Set the aspect ratio of the result. Image& AspectRatio(AspectRatio aspectRatio); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index a1dd269062..a57686ced3 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -16,6 +16,7 @@ #include #include #include +#include EXTERN_C IMAGE_DOS_HEADER __ImageBase; @@ -70,10 +71,7 @@ namespace AppInstaller::CLI::Workflow out << std::endl; } - // Determines icon fit given two options. - // Targets an 80x80 icon as the best resolution for this use case. - // TODO: Consider theme based on current background color. - bool IsSecondIconBetter(const Manifest::Icon& current, const Manifest::Icon& alternative) + bool IsSecondIconResolutionBetter(Manifest::IconResolutionEnum current, Manifest::IconResolutionEnum alternative) { static constexpr std::array s_iconResolutionOrder { @@ -95,7 +93,33 @@ namespace AppInstaller::CLI::Workflow 7, // Square256 }; - return s_iconResolutionOrder.at(ToIntegral(alternative.Resolution)) < s_iconResolutionOrder.at(ToIntegral(current.Resolution)); + return s_iconResolutionOrder.at(ToIntegral(alternative)) < s_iconResolutionOrder.at(ToIntegral(current)); + } + + // Determines icon fit given two options. + // Targets an 80x80 icon as the best resolution for this use case. + // TODO: Consider theme based on current background color. + bool IsSecondIconBetter(const Manifest::Icon& current, const Manifest::Icon& alternative) + { + return IsSecondIconResolutionBetter(current.Resolution, alternative.Resolution); + } + + bool IsSecondIconBetter(const ExtractedIconInfo& current, const ExtractedIconInfo& alternative) + { + return IsSecondIconResolutionBetter(current.IconResolution, alternative.IconResolution); + } + + void ShowIcon(Execution::OutputStream& outputStream, VirtualTerminal::Sixel::Image& icon) + { + // Using a height of 4 arbitrarily; allow width up to the entire console. + UINT imageHeightCells = 4; + UINT imageWidthCells = static_cast(Execution::GetConsoleWidth()); + + icon.RenderSizeInCells(imageWidthCells, imageHeightCells); + icon.RenderTo(outputStream); + + // Force the final sixel line to not be overwritten + outputStream << std::endl; } void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try @@ -127,17 +151,31 @@ namespace AppInstaller::CLI::Workflow auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); VirtualTerminal::Sixel::Image sixelIcon{ *iconStream, bestFitIcon->FileType }; + auto infoOut = context.Reporter.Info(); - // Using a height of 4 arbitrarily; allow width up to the entire console. - UINT imageHeightCells = 4; - UINT imageWidthCells = static_cast(Execution::GetConsoleWidth()); + ShowIcon(infoOut, sixelIcon); + } + CATCH_LOG(); - sixelIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); - auto infoOut = context.Reporter.Info(); - sixelIcon.RenderTo(infoOut); + void ShowExtractedIcon(Execution::OutputStream& outputStream, const std::vector& icons) try + { + const ExtractedIconInfo* bestFitIcon = nullptr; - // Force the final sixel line to not be overwritten - infoOut << std::endl; + for (const auto& icon : icons) + { + if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) + { + bestFitIcon = &icon; + } + } + + if (!bestFitIcon) + { + return; + } + + VirtualTerminal::Sixel::Image sixelIcon{ bestFitIcon->IconContent, bestFitIcon->IconFileType }; + ShowIcon(outputStream, sixelIcon); } CATCH_LOG(); @@ -344,6 +382,7 @@ namespace AppInstaller::CLI::Workflow auto info = context.Reporter.Info(); size_t packageIndex = 0; size_t totalLines = lines.size(); + bool shouldGetIcon = context.Reporter.SixelsEnabled(); for (const auto& line : lines) { // Identity header including package count indicator if multiple lines provided @@ -354,6 +393,23 @@ namespace AppInstaller::CLI::Workflow ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); + auto metadata = line.InstalledPackageVersion->GetMetadata(); + auto productCodes = line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + + if (shouldGetIcon && !productCodes.empty()) + { + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; + + auto itr = metadata.find(PackageVersionMetadata::InstalledScope); + if (itr != metadata.end()) + { + scope = Manifest::ConvertToScopeEnum(itr->second); + } + + auto icons = ExtractIconFromArpEntry(productCodes[0], scope); + ShowExtractedIcon(info, icons); + } + ShowSingleLineField(info, Resource::String::ShowLabelVersion, line.InstalledVersion); ShowSingleLineField(info, Resource::String::ShowLabelChannel, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Channel)); ShowSingleLineField(info, Resource::String::ShowLabelPublisher, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Publisher)); @@ -364,12 +420,10 @@ namespace AppInstaller::CLI::Workflow } ShowMultiValueField(info, Resource::String::ShowListPackageFamilyName, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName)); - ShowMultiValueField(info, Resource::String::ShowListProductCode, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode)); + ShowMultiValueField(info, Resource::String::ShowListProductCode, productCodes); ShowMultiValueField(info, Resource::String::ShowListUpgradeCode, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode)); - auto metadata = line.InstalledPackageVersion->GetMetadata(); - - ShowMetadataField(info, Resource::String::ShowLabelInstallerType, metadata, PackageVersionMetadata::InstalledType); + ShowMetadataField(info, Resource::String::ShowListInstallerCategory, metadata, PackageVersionMetadata::InstalledType); ShowMetadataField(info, Resource::String::ShowListInstalledScope, metadata, PackageVersionMetadata::InstalledScope); ShowMetadataField(info, Resource::String::ShowListInstalledArchitecture, metadata, PackageVersionMetadata::InstalledArchitecture); ShowMetadataField(info, Resource::String::ShowListInstalledLocale, metadata, PackageVersionMetadata::InstalledLocale); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index bbe612f82a..212869c0b4 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3506,4 +3506,8 @@ An unlocalized JSON fragment will follow on another line. Available Upgrades: Precedes a list of package upgrades available for the installed package. + + Installer Category: + Precedes a value that indicates the category of the installer for the installed package (ex. exe, msi, msix). + \ No newline at end of file From e1fdf44f3389631feec9252fee87025de9c30507 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 19 Dec 2025 10:55:49 -0800 Subject: [PATCH 7/7] settings schema and docs --- doc/Settings.md | 27 +++++++++++++++++++ .../JSON/settings/settings.schema.0.2.json | 10 +++++++ 2 files changed, 37 insertions(+) diff --git a/doc/Settings.md b/doc/Settings.md index 905b2f64dd..2beeced86c 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -403,3 +403,30 @@ This feature enables support for additional source command improvements via `win "sourceEdit": true }, ``` + +### listDetails + +This feature enables support for displaying detailed output from the `list` command. Rather than a table view of the results, when the `--details` option is provided +to the `list` command, it will output information similar to how `show` would. Most of the data presented is directly from the local installation. + +Example output: +```PowerShell +> winget list Microsoft.VisualStudio.2022.Enterprise --details +Visual Studio Enterprise 2022 [Microsoft.VisualStudio.2022.Enterprise] +Version: 17.14.21 (November 2025) +Publisher: Microsoft Corporation +Local Identifier: ARP\Machine\X86\875fed29 +Product Code: 875fed29 +Installer Category: exe +Installed Scope: Machine +Installed Location: C:\Program Files\Microsoft Visual Studio\2022\Enterprise +Available Upgrades: + winget [17.14.23] +``` + +To enable: +```json + "experimentalFeatures": { + "listDetails": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index daa289b75c..c8eb465cbf 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -328,6 +328,16 @@ "description": "Enable support for managing fonts", "type": "boolean", "default": false + }, + "listDetails": { + "description": "Enable detailed output option for list command", + "type": "boolean", + "default": false + }, + "sourceEdit": { + "description": "Enable source edit command", + "type": "boolean", + "default": false } } }