winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit d148be1e906235cd0beae7651776fc6cc2b73bf7
parent a0f486435eb2c5643e601096cff6bfbad5a6effc
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Mon, 30 Sep 2024 13:33:39 -0700

Sixel support (#4828)

## Hackathon
As part of a yearly event to work on personal choice projects, I
implemented support for rendering
[sixels](https://en.wikipedia.org/wiki/Sixel) in a few different
scenarios. You can try it out in any terminal that supports sixels, such
as the latest [Windows Terminal Preview
build](https://github.com/microsoft/terminal/releases/tag/v1.22.2362.0).
Both @lhecker and @j4james were very helpful with guidance on the
nuances of sixels.

## Change
The foundational work is a wrapper around the [Windows Imaging Component
(WIC) APIs](https://learn.microsoft.com/en-us/windows/win32/api/_wic/)
and a renderer for converting the indexed images to sixel format. WIC
does all the heavy lifting of decoding images, palette optimization and
dithering. My additions are a simple compositor and sixel renderer.
There is also some new code for detecting VT extension support,
including only enabling sixel rendering if the terminal advertises the
extension.

Sixel use was added in various ways. The winget icon is output any time
the header would be (during `winget --info` and any time we output
help):

<img width="435" alt="winget-info"
src="https://github.com/user-attachments/assets/7e6f8548-1c09-4108-aa5e-810bfeef6c4f">

The package icon (if present) is displayed during `winget show`. This is
an example using a local manifest updated to point to an image located
on the github repository of the package; actual package icon will likely
be different. Package icons extracted from actual installs are expected
to arrive soon:

<img width="266" alt="winget-show"
src="https://github.com/user-attachments/assets/fdbcf1fa-c2a3-4094-9415-4761702a44fa">

A new progress visualization is available using sixels. The indefinite
progress (the current character spinner that iterates through various
slashes, dash and pipe) looks like:


![winget-indefinite](https://github.com/user-attachments/assets/2f4e66e2-ecf6-4137-8d5b-ceddf4df11a2)

while the progress bar is a conveyor belt bringing your package to you:


![winget-progress](https://github.com/user-attachments/assets/c18b734c-808c-4ebd-a938-795cdde22154)
_The conveyor belt base image is only the finest of dev art; it is very
open to someone with more artistic skill to improve upon._

The tearing present in this gif does exist, but it is exaggerated by the
capture and the slow steady progress. Actual progress is much less
steady, making any tearing that may occur far less noticeable.

## Settings
Use of sixels (including even the attempt to detect support in the
terminal) is gated behind enabling them in the settings. Two new/updated
settings are available.

- `.visual.enableSixels` (`true` or `false`)
- This controls the winget icon during help and the package icon during
show.
- `.visual.progressBar`
  - `"sixel"` causes progress to use the sixel versions shown above.
  - `"disabled"` disables progress output.
Diffstat:
M.github/actions/spelling/allow.txt | 2++
M.github/actions/spelling/expect.txt | 3+++
Mdoc/Settings.md | 22++++++++++++++++------
Mschemas/JSON/settings/settings.schema.0.2.json | 9++++++++-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Msrc/AppInstallerCLICore/ChannelStreams.cpp | 13+++++++++++--
Msrc/AppInstallerCLICore/ChannelStreams.h | 5+++++
Msrc/AppInstallerCLICore/Command.cpp | 34+++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/Commands/DebugCommand.cpp | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/DebugCommand.h | 28++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionProgress.cpp | 780+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/AppInstallerCLICore/ExecutionProgress.h | 88+++++++++++++++++++++++--------------------------------------------------------
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 53++++++++++++++++++++++++++++++++++++-----------------
Msrc/AppInstallerCLICore/ExecutionReporter.h | 23++++++++++++++++-------
Asrc/AppInstallerCLICore/Sixel.cpp | 711+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Sixel.h | 262+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/VTSupport.cpp | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/AppInstallerCLICore/VTSupport.h | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/pch.h | 3+++
Msrc/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj | 3+++
Asrc/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png | 0
Asrc/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png | 0
Asrc/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png | 0
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Asrc/AppInstallerCLITests/Sixel.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Strings.cpp | 14++++++++++++++
Msrc/AppInstallerCommonCore/FileCache.cpp | 3+++
Msrc/AppInstallerCommonCore/Public/AppInstallerRuntime.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/FileCache.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ManifestLocalization.h | 4++--
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 4++++
Msrc/AppInstallerCommonCore/Runtime.cpp | 20++++++++++++++++++++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 20+++++++++++++-------
Msrc/AppInstallerSharedLib/AppInstallerStrings.cpp | 6++++++
Msrc/AppInstallerSharedLib/Public/AppInstallerStrings.h | 3+++
38 files changed, 2396 insertions(+), 363 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -302,6 +302,8 @@ silentwithprogress Silverlight simplesave simpletest +sixel +sixels sln sqlbuilder sqliteicu diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -252,6 +252,7 @@ ISQ ISVs iswgp itr +IWIC iwr iwgc JArray @@ -615,8 +616,10 @@ wesome wfsopen wgetenv Whatif +WIC wildcards WINAPI +wincodec windir windowsdeveloper winerror diff --git a/doc/Settings.md b/doc/Settings.md @@ -55,6 +55,16 @@ Replaces some known folder paths with their respective environment variable. Def }, ``` +### enableSixels + +Enables output of sixel images in certain contexts. Defaults to false. + +```json + "visual": { + "enableSixels": true + }, +``` + ## Install Behavior The `installBehavior` settings affect the default behavior of installing and upgrading (where applicable) packages. @@ -97,12 +107,12 @@ The 'skipDependencies' behavior affects whether dependencies are installed for a "installBehavior": { "skipDependencies": true }, -``` - -### Archive Extraction Method -The 'archiveExtractionMethod' behavior affects how installer archives are extracted. Currently there are two supported values: `Tar` or `ShellApi`. -`Tar` indicates that the archive should be extracted using the tar executable ('tar.exe') while `shellApi` indicates using the Windows Shell API. Defaults to `shellApi` if value is not set or is invalid. - +``` + +### Archive Extraction Method +The 'archiveExtractionMethod' behavior affects how installer archives are extracted. Currently there are two supported values: `Tar` or `ShellApi`. +`Tar` indicates that the archive should be extracted using the tar executable ('tar.exe') while `shellApi` indicates using the Windows Shell API. Defaults to `shellApi` if value is not set or is invalid. + ```json "installBehavior": { "archiveExtractionMethod": "tar" | "shellApi" diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -26,13 +26,20 @@ "enum": [ "accent", "rainbow", - "retro" + "retro", + "sixel", + "disabled" ] }, "anonymizeDisplayedPaths": { "description": "Replaces some known folder paths with their respective environment variable", "type": "boolean", "default": true + }, + "enableSixels": { + "description": "Enables output of sixel images in certain contexts", + "type": "boolean", + "default": false } } }, diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -389,6 +389,7 @@ <ClInclude Include="ContextOrchestrator.h" /> <ClInclude Include="COMContext.h" /> <ClInclude Include="Public\ConfigurationSetProcessorFactoryRemoting.h" /> + <ClInclude Include="Sixel.h" /> <ClInclude Include="Workflows\ConfigurationFlow.h" /> <ClInclude Include="Workflows\DependenciesFlow.h" /> <ClInclude Include="ExecutionArgs.h" /> @@ -451,6 +452,7 @@ <ClCompile Include="ConfigurationWingetDscModuleUnitValidation.cpp" /> <ClCompile Include="ConfigureExportCommand.cpp" /> <ClCompile Include="ContextOrchestrator.cpp" /> + <ClCompile Include="Sixel.cpp" /> <ClCompile Include="Workflows\ConfigurationFlow.cpp" /> <ClCompile Include="Workflows\DependenciesFlow.cpp" /> <ClCompile Include="PackageCollection.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -257,6 +257,9 @@ <ClInclude Include="Commands\ConfigureListCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Sixel.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -484,6 +487,9 @@ <ClCompile Include="Commands\ConfigureListCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClCompile Include="Sixel.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp @@ -71,6 +71,11 @@ namespace AppInstaller::CLI::Execution m_enabled = false; } + std::ostream& BaseStream::Get() + { + return m_out; + } + OutputStream::OutputStream(BaseStream& out, bool enabled, bool VTEnabled) : m_out(out), m_enabled(enabled), @@ -82,6 +87,11 @@ namespace AppInstaller::CLI::Execution m_format.Append(sequence); } + void OutputStream::ClearFormat() + { + m_format.Clear(); + } + void OutputStream::ApplyFormat() { // Only apply format if m_applyFormatAtOne == 1 coming into this function. @@ -152,4 +162,4 @@ namespace AppInstaller::CLI::Execution return *this; } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/ChannelStreams.h b/src/AppInstallerCLICore/ChannelStreams.h @@ -36,6 +36,8 @@ namespace AppInstaller::CLI::Execution void Disable(); + std::ostream& Get(); + private: template <typename T> void Write(const T& t, bool bypass) @@ -60,6 +62,9 @@ namespace AppInstaller::CLI::Execution // Adds a format to the current value. void AddFormat(const VirtualTerminal::Sequence& sequence); + // Clears the current format value. + void ClearFormat(); + template <typename T> OutputStream& operator<<(const T& t) { diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Command.h" #include "Resources.h" +#include "Sixel.h" #include <winget/UserSettings.h> #include <AppInstallerRuntime.h> #include <winget/Locale.h> @@ -42,8 +43,39 @@ namespace AppInstaller::CLI void Command::OutputIntroHeader(Execution::Reporter& reporter) const { + auto infoOut = reporter.Info(); + VirtualTerminal::ConstructedSequence indent; + + if (reporter.SixelsEnabled()) + { + try + { + std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); + + if (!imagePath.empty()) + { + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + imagePath /= "AppList.targetsize-40.png"; + + VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; + + // Using a height of 2 to match the two lines of header. + UINT imageHeightCells = 2; + UINT imageWidthCells = 2 * imageHeightCells; + + wingetIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); + wingetIcon.RenderTo(infoOut); + + indent = VirtualTerminal::Cursor::Position::Forward(static_cast<int16_t>(imageWidthCells)); + infoOut << VirtualTerminal::Cursor::Position::Up(static_cast<int16_t>(imageHeightCells) - 1); + } + } + CATCH_LOG(); + } + auto productName = Runtime::IsReleaseBuild() ? Resource::String::WindowsPackageManager : Resource::String::WindowsPackageManagerPreview; - reporter.Info() << productName(Runtime::GetClientVersion()) << std::endl << Resource::String::MainCopyrightNotice << std::endl; + infoOut << indent << productName(Runtime::GetClientVersion()) << std::endl + << indent << Resource::String::MainCopyrightNotice << std::endl; } void Command::OutputHelp(Execution::Reporter& reporter, const CommandException* exception) const diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -5,6 +5,10 @@ #if _DEBUG #include "DebugCommand.h" #include <winrt/Microsoft.Management.Configuration.h> +#include "AppInstallerDownloader.h" +#include "Sixel.h" + +using namespace AppInstaller::CLI::Execution; namespace AppInstaller::CLI { @@ -55,6 +59,8 @@ namespace AppInstaller::CLI std::make_unique<DumpProxyStubRegistrationsCommand>(FullName()), std::make_unique<DumpInterestingIIDsCommand>(FullName()), std::make_unique<DumpErrorResourceCommand>(FullName()), + std::make_unique<ShowSixelCommand>(FullName()), + std::make_unique<ProgressCommand>(FullName()), }); } @@ -148,6 +154,204 @@ namespace AppInstaller::CLI " </data>" << std::endl; } } + +#define WINGET_DEBUG_SIXEL_FILE Args::Type::Manifest +#define WINGET_DEBUG_SIXEL_ASPECT_RATIO Args::Type::AcceptPackageAgreements +#define WINGET_DEBUG_SIXEL_TRANSPARENT Args::Type::AcceptSourceAgreements +#define WINGET_DEBUG_SIXEL_COLOR_COUNT Args::Type::ConfigurationAcceptWarning +#define WINGET_DEBUG_SIXEL_WIDTH Args::Type::AdminSettingEnable +#define WINGET_DEBUG_SIXEL_HEIGHT Args::Type::AllowReboot +#define WINGET_DEBUG_SIXEL_STRETCH Args::Type::AllVersions +#define WINGET_DEBUG_SIXEL_REPEAT Args::Type::Name +#define WINGET_DEBUG_SIXEL_OUT_FILE Args::Type::BlockingPin + + std::vector<Argument> ShowSixelCommand::GetArguments() const + { + return { + Argument{ "file", 'f', WINGET_DEBUG_SIXEL_FILE, Resource::String::SourceListUpdatedNever, ArgumentType::Positional }, + Argument{ "aspect-ratio", 'a', WINGET_DEBUG_SIXEL_ASPECT_RATIO, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "transparent", 't', WINGET_DEBUG_SIXEL_TRANSPARENT, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "color-count", 'c', WINGET_DEBUG_SIXEL_COLOR_COUNT, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "width", 'w', WINGET_DEBUG_SIXEL_WIDTH, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "height", 'h', WINGET_DEBUG_SIXEL_HEIGHT, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "stretch", 's', WINGET_DEBUG_SIXEL_STRETCH, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "repeat", 'r', WINGET_DEBUG_SIXEL_REPEAT, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "out-file", 'o', WINGET_DEBUG_SIXEL_OUT_FILE, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + }; + } + + Resource::LocString ShowSixelCommand::ShortDescription() const + { + return Utility::LocIndString("Output an image with sixels"sv); + } + + Resource::LocString ShowSixelCommand::LongDescription() const + { + return Utility::LocIndString("Outputs an image from a file using sixel format."sv); + } + + void ShowSixelCommand::ExecuteInternal(Execution::Context& context) const + { + using namespace VirtualTerminal; + std::unique_ptr<Sixel::Image> sixelImagePtr; + + std::string imageUrl{ context.Args.GetArg(WINGET_DEBUG_SIXEL_FILE) }; + + if (Utility::IsUrlRemote(imageUrl)) + { + auto imageStream = std::make_unique<std::stringstream>(); + ProgressCallback emptyCallback; + Utility::DownloadToStream(imageUrl, *imageStream, Utility::DownloadType::Manifest, emptyCallback); + + sixelImagePtr = std::make_unique<Sixel::Image>(*imageStream, Manifest::IconFileTypeEnum::Unknown); + } + else + { + sixelImagePtr = std::make_unique<Sixel::Image>(Utility::ConvertToUTF16(imageUrl)); + } + + Sixel::Image& sixelImage = *sixelImagePtr.get(); + + if (context.Args.Contains(WINGET_DEBUG_SIXEL_ASPECT_RATIO)) + { + switch (context.Args.GetArg(WINGET_DEBUG_SIXEL_ASPECT_RATIO)[0]) + { + case '1': + sixelImage.AspectRatio(Sixel::AspectRatio::OneToOne); + break; + case '2': + sixelImage.AspectRatio(Sixel::AspectRatio::TwoToOne); + break; + case '3': + sixelImage.AspectRatio(Sixel::AspectRatio::ThreeToOne); + break; + case '5': + sixelImage.AspectRatio(Sixel::AspectRatio::FiveToOne); + break; + } + } + + sixelImage.Transparency(context.Args.Contains(WINGET_DEBUG_SIXEL_TRANSPARENT)); + + if (context.Args.Contains(WINGET_DEBUG_SIXEL_COLOR_COUNT)) + { + sixelImage.ColorCount(std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_SIXEL_COLOR_COUNT) })); + } + + if (context.Args.Contains(WINGET_DEBUG_SIXEL_WIDTH) && context.Args.Contains(WINGET_DEBUG_SIXEL_HEIGHT)) + { + sixelImage.RenderSizeInCells( + std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_SIXEL_WIDTH) }), + std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_SIXEL_HEIGHT) })); + } + + sixelImage.StretchSourceToFill(context.Args.Contains(WINGET_DEBUG_SIXEL_STRETCH)); + + sixelImage.UseRepeatSequence(context.Args.Contains(WINGET_DEBUG_SIXEL_REPEAT)); + + if (context.Args.Contains(WINGET_DEBUG_SIXEL_OUT_FILE)) + { + std::ofstream stream{ Utility::ConvertToUTF16(context.Args.GetArg(WINGET_DEBUG_SIXEL_OUT_FILE)) }; + stream << sixelImage.Render().Get(); + } + else + { + OutputStream stream = context.Reporter.GetOutputStream(Reporter::Level::Info); + stream.ClearFormat(); + sixelImage.RenderTo(stream); + + // Force a new line to show entire image + stream << std::endl; + } + } + +#define WINGET_DEBUG_PROGRESS_SIXEL Args::Type::Manifest +#define WINGET_DEBUG_PROGRESS_DISABLED Args::Type::GatedVersion +#define WINGET_DEBUG_PROGRESS_HIDE Args::Type::AcceptPackageAgreements +#define WINGET_DEBUG_PROGRESS_TIME Args::Type::AcceptSourceAgreements +#define WINGET_DEBUG_PROGRESS_MESSAGE Args::Type::ConfigurationAcceptWarning +#define WINGET_DEBUG_PROGRESS_PERCENT Args::Type::AllowReboot +#define WINGET_DEBUG_PROGRESS_POST Args::Type::AllVersions + + std::vector<Argument> ProgressCommand::GetArguments() const + { + return { + Argument{ "sixel", 's', WINGET_DEBUG_PROGRESS_SIXEL, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "disabled", 'd', WINGET_DEBUG_PROGRESS_DISABLED, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "hide", 'h', WINGET_DEBUG_PROGRESS_HIDE, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "time", 't', WINGET_DEBUG_PROGRESS_TIME, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "message", 'm', WINGET_DEBUG_PROGRESS_MESSAGE, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "percent", 'p', WINGET_DEBUG_PROGRESS_PERCENT, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "post", 0, WINGET_DEBUG_PROGRESS_POST, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + }; + } + + Resource::LocString ProgressCommand::ShortDescription() const + { + return Utility::LocIndString("Show progress"sv); + } + + Resource::LocString ProgressCommand::LongDescription() const + { + return Utility::LocIndString("Show progress with various controls to emulate different behaviors."sv); + } + + void ProgressCommand::ExecuteInternal(Execution::Context& context) const + { + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_SIXEL)) + { + context.Reporter.SetStyle(Settings::VisualStyle::Sixel); + } + + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_DISABLED)) + { + context.Reporter.SetStyle(Settings::VisualStyle::Disabled); + } + + auto progress = context.Reporter.BeginAsyncProgress(context.Args.Contains(WINGET_DEBUG_PROGRESS_HIDE)); + + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_MESSAGE)) + { + progress->Callback().SetProgressMessage(context.Args.GetArg(WINGET_DEBUG_PROGRESS_MESSAGE)); + } + + bool sendProgress = context.Args.Contains(WINGET_DEBUG_PROGRESS_PERCENT); + + UINT timeInSeconds = 3600; + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_TIME)) + { + timeInSeconds = std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_PROGRESS_TIME) }); + } + + UINT ticks = timeInSeconds * 10; + for (UINT i = 0; i < ticks; ++i) + { + if (sendProgress) + { + progress->Callback().OnProgress(i, ticks, ProgressType::Bytes); + } + + if (progress->Callback().IsCancelledBy(CancelReason::Any)) + { + sendProgress = false; + break; + } + + std::this_thread::sleep_for(100ms); + } + + if (sendProgress) + { + progress->Callback().OnProgress(ticks, ticks, ProgressType::Bytes); + } + + progress.reset(); + + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_POST)) + { + context.Reporter.Info() << context.Args.GetArg(WINGET_DEBUG_PROGRESS_POST) << std::endl; + } + } } #endif diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.h b/src/AppInstallerCLICore/Commands/DebugCommand.h @@ -57,6 +57,34 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + + // Outputs a sixel image. + struct ShowSixelCommand final : public Command + { + ShowSixelCommand(std::string_view parent) : Command("sixel", {}, parent) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; + + // Invokes progress display. + struct ProgressCommand final : public Command + { + ProgressCommand(std::string_view parent) : Command("progress", {}, parent) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; } #endif diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -2,15 +2,20 @@ // Licensed under the MIT License. #include "pch.h" #include "ExecutionProgress.h" +#include "VTSupport.h" +#include "AppInstallerRuntime.h" +#include "Sixel.h" + +using namespace AppInstaller::Settings; +using namespace AppInstaller::CLI::VirtualTerminal; +using namespace std::string_view_literals; namespace AppInstaller::CLI::Execution { - using namespace Settings; - using namespace VirtualTerminal; - using namespace std::string_view_literals; - namespace { + static constexpr size_t s_ProgressBarCellWidth = 30; + struct BytesFormatData { uint64_t PowerOfTwo; @@ -127,11 +132,57 @@ namespace AppInstaller::CLI::Execution } } - namespace details + // Shared functionality for progress visualizers. + struct ProgressVisualizerBase + { + ProgressVisualizerBase(BaseStream& stream, bool enableVT) : + m_out(stream), m_enableVT(enableVT) {} + + void SetMessage(std::string_view message) + { + std::atomic_store(&m_message, std::make_shared<Utility::NormalizedString>(message)); + } + + std::shared_ptr<Utility::NormalizedString> Message() + { + return std::atomic_load(&m_message); + } + + protected: + BaseStream& m_out; + + bool VT_Enabled() const { return m_enableVT; } + + void ClearLine() + { + if (VT_Enabled()) + { + m_out << TextModification::EraseLineEntirely << '\r'; + } + else + { + m_out << '\r' << std::string(GetConsoleWidth(), ' ') << '\r'; + } + } + + private: + bool m_enableVT = false; + std::shared_ptr<Utility::NormalizedString> m_message; + }; + + // Shared functionality for progress visualizers. + struct CharacterProgressVisualizerBase : public ProgressVisualizerBase { - void ProgressVisualizerBase::ApplyStyle(size_t i, size_t max, bool foregroundOnly) + CharacterProgressVisualizerBase(BaseStream& stream, bool enableVT, VisualStyle style) : + ProgressVisualizerBase(stream, enableVT && style != AppInstaller::Settings::VisualStyle::NoVT), m_style(style) {} + + protected: + Settings::VisualStyle m_style = AppInstaller::Settings::VisualStyle::Accent; + + // Applies the selected visual style. + void ApplyStyle(size_t i, size_t max, bool foregroundOnly) { - if (!UseVT()) + if (!VT_Enabled()) { // Either no style set or VT disabled return; @@ -151,289 +202,628 @@ namespace AppInstaller::CLI::Execution LOG_HR(E_UNEXPECTED); } } + }; - void ProgressVisualizerBase::ClearLine() + // Displays an indefinite spinner via a character. + struct CharacterIndefiniteSpinner : public CharacterProgressVisualizerBase, public IIndefiniteSpinner + { + CharacterIndefiniteSpinner(BaseStream& stream, bool enableVT, VisualStyle style) : + CharacterProgressVisualizerBase(stream, enableVT, style) {} + + void ShowSpinner() override { - if (UseVT()) + if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) { - m_out << TextModification::EraseLineEntirely << '\r'; + m_spinnerRunning = true; + m_spinnerJob = std::async(std::launch::async, &CharacterIndefiniteSpinner::ShowSpinnerInternal, this); } - else + } + + void StopSpinner() override + { + if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) { - m_out << '\r' << std::string(GetConsoleWidth(), ' ') << '\r'; + m_canceled = true; + m_spinnerJob.get(); } } - void ProgressVisualizerBase::Message(std::string_view message) + void SetMessage(std::string_view message) override { - std::atomic_store(&m_message, std::make_shared<Utility::NormalizedString>(message)); + ProgressVisualizerBase::SetMessage(message); } - std::shared_ptr<Utility::NormalizedString> ProgressVisualizerBase::Message() + std::shared_ptr<Utility::NormalizedString> Message() override { - return std::atomic_load(&m_message); + return ProgressVisualizerBase::Message(); } - } - void IndefiniteSpinner::ShowSpinner() - { - if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) + private: + std::atomic<bool> m_canceled = false; + std::atomic<bool> m_spinnerRunning = false; + std::future<void> m_spinnerJob; + + void ShowSpinnerInternal() { - m_spinnerRunning = true; - m_spinnerJob = std::async(std::launch::async, &IndefiniteSpinner::ShowSpinnerInternal, this); + char spinnerChars[] = { '-', '\\', '|', '/' }; + + // First wait for a small amount of time to enable a fast task to skip + // showing anything, or a progress task to skip straight to progress. + Sleep(100); + + if (!m_canceled) + { + if (VT_Enabled()) + { + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Indeterminate); + } + + // Indent two spaces for the spinner, but three here so that we can overwrite it in the loop. + std::string_view indent = " "; + std::shared_ptr<Utility::NormalizedString> message = ProgressVisualizerBase::Message(); + size_t messageLength = message ? Utility::UTF8ColumnWidth(*message) : 0; + + for (size_t i = 0; !m_canceled; ++i) + { + constexpr size_t repetitionCount = 20; + ApplyStyle(i % repetitionCount, repetitionCount, true); + m_out << '\r' << indent << spinnerChars[i % ARRAYSIZE(spinnerChars)]; + m_out.RestoreDefault(); + + std::shared_ptr<Utility::NormalizedString> newMessage = ProgressVisualizerBase::Message(); + std::string eraser; + if (newMessage) + { + size_t newLength = Utility::UTF8ColumnWidth(*newMessage); + + if (newLength < messageLength) + { + eraser = std::string(messageLength - newLength, ' '); + } + + message = newMessage; + messageLength = newLength; + } + + m_out << ' ' << (message ? *message : std::string{}) << eraser << std::flush; + Sleep(250); + } + + ClearLine(); + + if (VT_Enabled()) + { + m_out << Progress::Construct(Progress::State::None); + } + } + + m_canceled = false; + m_spinnerRunning = false; } - } + }; - void IndefiniteSpinner::StopSpinner() + // Displays progress via character output. + class CharacterProgressBar : public CharacterProgressVisualizerBase, public IProgressBar { - if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + public: + CharacterProgressBar(BaseStream& stream, bool enableVT, VisualStyle style) : + CharacterProgressVisualizerBase(stream, enableVT, style) {} + + void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) override { - m_canceled = true; - m_spinnerJob.get(); - } - } + if (current < m_lastCurrent) + { + ClearLine(); + } - void IndefiniteSpinner::ShowSpinnerInternal() - { - char spinnerChars[] = { '-', '\\', '|', '/' }; + // TODO: Progress bar does not currently use message + if (VT_Enabled()) + { + ShowProgressWithVT(current, maximum, type); + } + else + { + ShowProgressNoVT(current, maximum, type); + } - // First wait for a small amount of time to enable a fast task to skip - // showing anything, or a progress task to skip straight to progress. - Sleep(100); + m_lastCurrent = current; + m_isVisible = true; + } - if (!m_canceled) + void EndProgress(bool hideProgressWhenDone) override { - if (UseVT()) + if (m_isVisible) { - // Additional VT-based progress reporting, for terminals that support it - m_out << Progress::Construct(Progress::State::Indeterminate); + if (hideProgressWhenDone) + { + ClearLine(); + } + else + { + m_out << std::endl; + } + + if (VT_Enabled()) + { + // We always clear the VT-based progress bar, even if hideProgressWhenDone is false + // since it would be confusing for users if progress continues to be shown after winget exits + // (it is typically not automatically cleared by terminals on process exit) + m_out << Progress::Construct(Progress::State::None); + } + + m_isVisible = false; } + } - // Indent two spaces for the spinner, but three here so that we can overwrite it in the loop. - std::string_view indent = " "; - std::shared_ptr<Utility::NormalizedString> message = this->Message(); - size_t messageLength = message ? Utility::UTF8ColumnWidth(*message) : 0; + private: + std::atomic<bool> m_isVisible = false; + uint64_t m_lastCurrent = 0; - for (size_t i = 0; !m_canceled; ++i) + void ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type) + { + m_out << "\r "; + + if (maximum) { - constexpr size_t repetitionCount = 20; - ApplyStyle(i % repetitionCount, repetitionCount, true); - m_out << '\r' << indent << spinnerChars[i % ARRAYSIZE(spinnerChars)]; - m_out.RestoreDefault(); + const char* const blockOn = u8"\x2588"; + const char* const blockOff = u8"\x2592"; + constexpr size_t blockWidth = 30; - std::shared_ptr<Utility::NormalizedString> newMessage = this->Message(); - std::string eraser; - if (newMessage) + double percentage = static_cast<double>(current) / maximum; + size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); + + for (size_t i = 0; i < blocksOn; ++i) { - size_t newLength = Utility::UTF8ColumnWidth(*newMessage); + m_out << blockOn; + } - if (newLength < messageLength) + for (size_t i = 0; i < blockWidth - blocksOn; ++i) + { + m_out << blockOff; + } + + m_out << " "; + + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + m_out << " / "; + OutputBytes(m_out, maximum); + break; + case AppInstaller::ProgressType::Percent: + default: + m_out << static_cast<int>(percentage * 100) << '%'; + break; + } + } + else + { + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + break; + case AppInstaller::ProgressType::Percent: + m_out << current << '%'; + break; + default: + m_out << current << " unknowns"; + break; + } + } + } + + void ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type) + { + m_out << TextFormat::Default; + + m_out << "\r "; + + if (maximum) + { + const char* const blocks[] = + { + u8" ", // block off + u8"\x258F", // block 1/8 + u8"\x258E", // block 2/8 + u8"\x258D", // block 3/8 + u8"\x258C", // block 4/8 + u8"\x258B", // block 5/8 + u8"\x258A", // block 6/8 + u8"\x2589", // block 7/8 + u8"\x2588" // block on + }; + const char* const blockOn = blocks[8]; + const char* const blockOff = blocks[0]; + constexpr size_t blockWidth = s_ProgressBarCellWidth; + + double percentage = static_cast<double>(current) / maximum; + size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); + size_t partialBlockIndex = static_cast<size_t>((percentage * blockWidth - blocksOn) * 8); + TextFormat::Color accent = TextFormat::Color::GetAccentColor(); + + for (size_t i = 0; i < blockWidth; ++i) + { + ApplyStyle(i, blockWidth, false); + + if (i < blocksOn) { - eraser = std::string(messageLength - newLength, ' '); + m_out << blockOn; + } + else if (i == blocksOn) + { + m_out << blocks[partialBlockIndex]; } + else + { + m_out << blockOff; + } + } - message = newMessage; - messageLength = newLength; + m_out << TextFormat::Default; + + m_out << " "; + + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + m_out << " / "; + OutputBytes(m_out, maximum); + break; + case AppInstaller::ProgressType::Percent: + default: + m_out << static_cast<int>(percentage * 100) << '%'; + break; } - m_out << ' ' << (message ? *message : std::string{}) << eraser << std::flush; - Sleep(250); + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Normal, static_cast<int>(percentage * 100)); + } + else + { + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + break; + case AppInstaller::ProgressType::Percent: + m_out << current << '%'; + break; + default: + m_out << current << " unknowns"; + break; + } } + } + }; + + // Displays an indefinite spinner via a sixel. + struct SixelIndefiniteSpinner : public ProgressVisualizerBase, public IIndefiniteSpinner + { + SixelIndefiniteSpinner(BaseStream& stream, bool enableVT) : + ProgressVisualizerBase(stream, enableVT) + { + Sixel::RenderControls& renderControls = m_compositor.Controls(); + renderControls.RenderSizeInCells(2, 1); + + // Create palette from full image + std::filesystem::path imageAssetsRoot = Runtime::GetPathTo(Runtime::PathName::ImageAssets); + THROW_WIN32_IF(ERROR_FILE_NOT_FOUND, imageAssetsRoot.empty()); + + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + Sixel::ImageSource wingetIcon{ imageAssetsRoot / "AppList.targetsize-20.png" }; + wingetIcon.Resize(renderControls); + Sixel::Palette palette = wingetIcon.CreatePalette(renderControls); + + m_folder = Sixel::ImageSource{ imageAssetsRoot / "progress-sixel/folders_only.png" }; + m_arrow = Sixel::ImageSource{ imageAssetsRoot / "progress-sixel/arrow_only.png" }; + + m_folder.Resize(renderControls); + m_folder.ApplyPalette(palette); + + Sixel::RenderControls arrowControls = renderControls; + arrowControls.InterpolationMode = Sixel::InterpolationMode::Linear; + m_arrow.Resize(arrowControls); + m_arrow.ApplyPalette(palette); - ClearLine(); + m_compositor.Palette(std::move(palette)); + m_compositor.AddView(m_arrow.Copy()); + m_compositor.AddView(m_folder.Copy()); + } - if (UseVT()) + void ShowSpinner() override + { + if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) { - m_out << Progress::Construct(Progress::State::None); + m_spinnerRunning = true; + m_spinnerJob = std::async(std::launch::async, &SixelIndefiniteSpinner::ShowSpinnerInternal, this); } } - m_canceled = false; - m_spinnerRunning = false; - } - - void ProgressBar::ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) - { - if (current < m_lastCurrent) + void StopSpinner() override { - ClearLine(); + if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + { + m_canceled = true; + m_spinnerJob.get(); + } } - // TODO: Progress bar does not currently use message - if (UseVT()) + void SetMessage(std::string_view message) override { - ShowProgressWithVT(current, maximum, type); + ProgressVisualizerBase::SetMessage(message); } - else + + std::shared_ptr<Utility::NormalizedString> Message() override { - ShowProgressNoVT(current, maximum, type); + return ProgressVisualizerBase::Message(); } - m_lastCurrent = current; - m_isVisible = true; - } + private: + std::atomic<bool> m_canceled = false; + std::atomic<bool> m_spinnerRunning = false; + std::future<void> m_spinnerJob; + Sixel::ImageSource m_folder; + Sixel::ImageSource m_arrow; + Sixel::Compositor m_compositor; - void ProgressBar::EndProgress(bool hideProgressWhenDone) - { - if (m_isVisible) + void ShowSpinnerInternal() { - if (hideProgressWhenDone) + // First wait for a small amount of time to enable a fast task to skip + // showing anything, or a progress task to skip straight to progress. + Sleep(100); + + if (!m_canceled) { + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Indeterminate); + + // Indent two spaces for the spinner, but three here so that we can overwrite it in the loop. + std::string_view indent = " "; + std::shared_ptr<Utility::NormalizedString> message = ProgressVisualizerBase::Message(); + size_t messageLength = message ? Utility::UTF8ColumnWidth(*message) : 0; + + UINT imageHeight = m_compositor.Controls().PixelHeight; + + for (size_t i = 0; !m_canceled; ++i) + { + m_out << '\r' << indent; + + // Move arrow down one pixel each time + m_compositor[0].Translate(0, i % imageHeight, true); + m_compositor.RenderTo(m_out); + + message = ProgressVisualizerBase::Message(); + size_t newLength = (message ? Utility::UTF8ColumnWidth(*message) : 0); + + std::string eraser; + if (newLength < messageLength) + { + eraser = std::string(messageLength - newLength, ' '); + } + + messageLength = newLength; + + m_out << VirtualTerminal::Cursor::Position::Forward(3) << (message ? *message : std::string{}) << eraser << std::flush; + Sleep(100); + } + ClearLine(); - } - else - { - m_out << std::endl; - } - if (UseVT()) - { - // We always clear the VT-based progress bar, even if hideProgressWhenDone is false - // since it would be confusing for users if progress continues to be shown after winget exits - // (it is typically not automatically cleared by terminals on process exit) m_out << Progress::Construct(Progress::State::None); } - m_isVisible = false; + m_canceled = false; + m_spinnerRunning = false; } - } + }; - void ProgressBar::ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type) + // Displays progress with a sixel image. + class SixelProgressBar : public ProgressVisualizerBase, public IProgressBar { - m_out << "\r "; - - if (maximum) + public: + SixelProgressBar(BaseStream& stream, bool enableVT) : + ProgressVisualizerBase(stream, enableVT) { - const char* const blockOn = u8"\x2588"; - const char* const blockOff = u8"\x2592"; - constexpr size_t blockWidth = 30; + static constexpr UINT s_colorsForBelt = 20; - double percentage = static_cast<double>(current) / maximum; - size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); + Sixel::RenderControls imageRenderControls; + imageRenderControls.RenderSizeInCells(2, 1); - for (size_t i = 0; i < blocksOn; ++i) - { - m_out << blockOn; - } + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + std::filesystem::path imageAssetsRoot = Runtime::GetPathTo(Runtime::PathName::ImageAssets); + THROW_WIN32_IF(ERROR_FILE_NOT_FOUND, imageAssetsRoot.empty()); - for (size_t i = 0; i < blockWidth - blocksOn; ++i) - { - m_out << blockOff; - } + m_icon = Sixel::ImageSource{ imageAssetsRoot / "AppList.targetsize-20.png" }; + m_icon.Resize(imageRenderControls); + imageRenderControls.ColorCount = Sixel::Palette::MaximumColorCount - s_colorsForBelt; + Sixel::Palette iconPalette = m_icon.CreatePalette(imageRenderControls); - m_out << " "; + // TODO: Move to real location + m_belt = Sixel::ImageSource{ imageAssetsRoot / "progress-sixel/conveyor.png" }; + m_belt.Resize(imageRenderControls); + imageRenderControls.ColorCount = s_colorsForBelt; + imageRenderControls.InterpolationMode = Sixel::InterpolationMode::Linear; + Sixel::Palette beltPalette = m_belt.CreatePalette(imageRenderControls); - switch (type) - { - case AppInstaller::ProgressType::Bytes: - OutputBytes(m_out, current); - m_out << " / "; - OutputBytes(m_out, maximum); - break; - case AppInstaller::ProgressType::Percent: - default: - m_out << static_cast<int>(percentage * 100) << '%'; - break; - } + Sixel::Palette combinedPalette{ iconPalette, beltPalette }; + + m_icon.ApplyPalette(combinedPalette); + m_belt.ApplyPalette(combinedPalette); + + m_compositor.Palette(std::move(combinedPalette)); + m_compositor.AddView(m_icon.Copy()); + m_compositor.AddView(m_belt.Copy()); + m_compositor.Controls().TransparencyEnabled = false; + m_compositor.Controls().RenderSizeInCells(s_ProgressBarCellWidth, 1); } - else + + void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) override { - switch (type) + if (current < m_lastCurrent) { - case AppInstaller::ProgressType::Bytes: - OutputBytes(m_out, current); - break; - case AppInstaller::ProgressType::Percent: - m_out << current << '%'; - break; - default: - m_out << current << " unknowns"; - break; + ClearLine(); } - } - } - void ProgressBar::ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type) - { - m_out << TextFormat::Default; + m_out << TextFormat::Default; - m_out << "\r "; + m_out << "\r "; - if (maximum) - { - const char* const blocks[] = - { - u8" ", // block off - u8"\x258F", // block 1/8 - u8"\x258E", // block 2/8 - u8"\x258D", // block 3/8 - u8"\x258C", // block 4/8 - u8"\x258B", // block 5/8 - u8"\x258A", // block 6/8 - u8"\x2589", // block 7/8 - u8"\x2588" // block on - }; - const char* const blockOn = blocks[8]; - const char* const blockOff = blocks[0]; - constexpr size_t blockWidth = 30; + if (maximum) + { - double percentage = static_cast<double>(current) / maximum; - size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); - size_t partialBlockIndex = static_cast<size_t>((percentage * blockWidth - blocksOn) * 8); - TextFormat::Color accent = TextFormat::Color::GetAccentColor(); + double percentage = static_cast<double>(current) / maximum; - for (size_t i = 0; i < blockWidth; ++i) - { - ApplyStyle(i, blockWidth, false); + // Translate icon so that its leading edge is the progress line + INT translation = static_cast<INT>((percentage * m_compositor.Controls().PixelWidth) - m_compositor[0].Width()); - if (i < blocksOn) - { - m_out << blockOn; + m_compositor[0].Translate(translation, 0, false); + m_compositor[1].Translate(translation, 0, true); + m_compositor.RenderTo(m_out); + + m_out << VirtualTerminal::Cursor::Position::Forward(s_ProgressBarCellWidth + 2); + + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + m_out << " / "; + OutputBytes(m_out, maximum); + break; + case AppInstaller::ProgressType::Percent: + default: + m_out << static_cast<int>(percentage * 100) << '%'; + break; + } + + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Normal, static_cast<int>(percentage * 100)); + } + else + { + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + break; + case AppInstaller::ProgressType::Percent: + m_out << current << '%'; + break; + default: + m_out << current << " unknowns"; + break; } - else if (i == blocksOn) + } + + m_lastCurrent = current; + m_isVisible = true; + } + + void EndProgress(bool hideProgressWhenDone) override + { + if (m_isVisible) + { + if (hideProgressWhenDone) { - m_out << blocks[partialBlockIndex]; + ClearLine(); } else { - m_out << blockOff; + m_out << std::endl; + } + + if (VT_Enabled()) + { + // We always clear the VT-based progress bar, even if hideProgressWhenDone is false + // since it would be confusing for users if progress continues to be shown after winget exits + // (it is typically not automatically cleared by terminals on process exit) + m_out << Progress::Construct(Progress::State::None); } + + m_isVisible = false; } + } - m_out << TextFormat::Default; + private: + std::atomic<bool> m_isVisible = false; + uint64_t m_lastCurrent = 0; + Sixel::ImageSource m_icon; + Sixel::ImageSource m_belt; + Sixel::Compositor m_compositor; + }; - m_out << " "; + std::unique_ptr<IIndefiniteSpinner> IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function<bool()>& sixelSupported) + { + std::unique_ptr<IIndefiniteSpinner> result; - switch (type) + switch (style) + { + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: + result = std::make_unique<CharacterIndefiniteSpinner>(stream, enableVT, style); + break; + case VisualStyle::Sixel: + if (sixelSupported()) { - case AppInstaller::ProgressType::Bytes: - OutputBytes(m_out, current); - m_out << " / "; - OutputBytes(m_out, maximum); - break; - case AppInstaller::ProgressType::Percent: - default: - m_out << static_cast<int>(percentage * 100) << '%'; - break; + try + { + result = std::make_unique<SixelIndefiniteSpinner>(stream, enableVT); + } + CATCH_LOG(); } - // Additional VT-based progress reporting, for terminals that support it - m_out << Progress::Construct(Progress::State::Normal, static_cast<int>(percentage * 100)); + if (!result) + { + result = std::make_unique<CharacterIndefiniteSpinner>(stream, enableVT, VisualStyle::Accent); + } + break; + case VisualStyle::Disabled: + break; + default: + THROW_HR(E_NOTIMPL); } - else + + return result; + } + + std::unique_ptr<IProgressBar> IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function<bool()>& sixelSupported) + { + std::unique_ptr<IProgressBar> result; + + switch (style) { - switch (type) + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: + result = std::make_unique<CharacterProgressBar>(stream, enableVT, style); + break; + case VisualStyle::Sixel: + if (sixelSupported()) { - case AppInstaller::ProgressType::Bytes: - OutputBytes(m_out, current); - break; - case AppInstaller::ProgressType::Percent: - m_out << current << '%'; - break; - default: - m_out << current << " unknowns"; - break; + try + { + result = std::make_unique<SixelProgressBar>(stream, enableVT); + } + CATCH_LOG(); } + + if (!result) + { + result = std::make_unique<CharacterProgressBar>(stream, enableVT, VisualStyle::Accent); + } + break; + case VisualStyle::Disabled: + break; + default: + THROW_HR(E_NOTIMPL); } + + return result; } } diff --git a/src/AppInstallerCLICore/ExecutionProgress.h b/src/AppInstallerCLICore/ExecutionProgress.h @@ -1,89 +1,51 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "VTSupport.h" +#include "ChannelStreams.h" #include <AppInstallerProgress.h> #include <AppInstallerStrings.h> #include <winget/UserSettings.h> -#include <ChannelStreams.h> -#include <wil/resource.h> - -#include <atomic> -#include <future> -#include <istream> +#include <functional> #include <memory> -#include <ostream> #include <string> -#include <vector> +#include <string_view> namespace AppInstaller::CLI::Execution { - namespace details - { - // Shared functionality for progress visualizers. - struct ProgressVisualizerBase - { - ProgressVisualizerBase(BaseStream& stream, bool enableVT) : - m_out(stream), m_enableVT(enableVT) {} - - void SetStyle(AppInstaller::Settings::VisualStyle style) { m_style = style; } - - void Message(std::string_view message); - std::shared_ptr<Utility::NormalizedString> Message(); - - protected: - BaseStream& m_out; - Settings::VisualStyle m_style = AppInstaller::Settings::VisualStyle::Accent; - - bool UseVT() const { return m_enableVT && m_style != AppInstaller::Settings::VisualStyle::NoVT; } - - // Applies the selected visual style. - void ApplyStyle(size_t i, size_t max, bool foregroundOnly); - - void ClearLine(); - - private: - bool m_enableVT = false; - std::shared_ptr<Utility::NormalizedString> m_message; - }; - } - // Displays an indefinite spinner. - struct IndefiniteSpinner : public details::ProgressVisualizerBase + struct IIndefiniteSpinner { - IndefiniteSpinner(BaseStream& stream, bool enableVT) : - details::ProgressVisualizerBase(stream, enableVT) {} + virtual ~IIndefiniteSpinner() = default; - void ShowSpinner(); + // Set the message for the spinner. + virtual void SetMessage(std::string_view message) = 0; - void StopSpinner(); + // Get the current message for the spinner. + virtual std::shared_ptr<Utility::NormalizedString> Message() = 0; - private: - std::atomic<bool> m_canceled = false; - std::atomic<bool> m_spinnerRunning = false; - std::future<void> m_spinnerJob; + // Show the indefinite spinner. + virtual void ShowSpinner() = 0; - void ShowSpinnerInternal(); + // Stop showing the indefinite spinner. + virtual void StopSpinner() = 0; + + // Creates an indefinite spinner for the given style. + static std::unique_ptr<IIndefiniteSpinner> CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, const std::function<bool()>& sixelSupported); }; - // Displays progress - class ProgressBar : public details::ProgressVisualizerBase + // Displays a progress bar. + struct IProgressBar { - public: - ProgressBar(BaseStream& stream, bool enableVT) : - details::ProgressVisualizerBase(stream, enableVT) {} - - void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type); - - void EndProgress(bool hideProgressWhenDone); + virtual ~IProgressBar() = default; - private: - std::atomic<bool> m_isVisible = false; - uint64_t m_lastCurrent = 0; + // Show progress with the given values. + virtual void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) = 0; - void ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type); + // Stop showing progress. + virtual void EndProgress(bool hideProgressWhenDone) = 0; - void ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type); + // Creates a progress bar for the given style. + static std::unique_ptr<IProgressBar> CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, const std::function<bool()>& sixelSupported); }; } diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -31,10 +31,12 @@ namespace AppInstaller::CLI::Execution Reporter::Reporter(std::shared_ptr<BaseStream> outStream, std::istream& inStream) : m_out(outStream), - m_in(inStream), - m_progressBar(std::in_place, *m_out, ConsoleModeRestore::Instance().IsVTEnabled()), - m_spinner(std::in_place, *m_out, ConsoleModeRestore::Instance().IsVTEnabled()) + m_in(inStream) { + auto sixelSupported = [&]() { return SixelsSupported(); }; + m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent, sixelSupported); + m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent, sixelSupported); + SetProgressSink(this); } @@ -52,6 +54,18 @@ namespace AppInstaller::CLI::Execution } } + std::optional<PrimaryDeviceAttributes> Reporter::GetPrimaryDeviceAttributes() + { + if (ConsoleModeRestore::Instance().IsVTEnabled()) + { + return PrimaryDeviceAttributes{ m_out->Get(), m_in }; + } + else + { + return std::nullopt; + } + } + OutputStream Reporter::GetOutputStream(Level level) { // If the level is not enabled, return a default stream which is disabled @@ -103,14 +117,14 @@ namespace AppInstaller::CLI::Execution void Reporter::SetStyle(VisualStyle style) { m_style = style; - if (m_spinner) - { - m_spinner->SetStyle(style); - } - if (m_progressBar) + + if (m_channel == Channel::Output) { - m_progressBar->SetStyle(style); + auto sixelSupported = [&]() { return SixelsSupported(); }; + m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style, sixelSupported); + m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style, sixelSupported); } + if (style == VisualStyle::NoVT) { m_out->SetVTEnabled(false); @@ -244,12 +258,7 @@ namespace AppInstaller::CLI::Execution { if (m_spinner) { - m_spinner->Message(message); - } - - if (m_progressBar) - { - m_progressBar->Message(message); + m_spinner->SetMessage(message); } } @@ -353,4 +362,15 @@ namespace AppInstaller::CLI::Execution WI_ClearAllFlags(m_enabledLevels, reporterLevel); } } -}- \ No newline at end of file + + bool Reporter::SixelsSupported() + { + auto attributes = GetPrimaryDeviceAttributes(); + return (attributes ? attributes->Supports(PrimaryDeviceAttributes::Extension::Sixel) : false); + } + + bool Reporter::SixelsEnabled() + { + return Settings::User().Get<Settings::Setting::EnableSixelDisplay>() && SixelsSupported(); + } +} diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -72,6 +72,9 @@ namespace AppInstaller::CLI::Execution ~Reporter(); + // Gets the primary device attributes if available. + std::optional<VirtualTerminal::PrimaryDeviceAttributes> GetPrimaryDeviceAttributes(); + // Get a stream for verbose output. OutputStream Verbose() { return GetOutputStream(Level::Verbose); } @@ -108,11 +111,6 @@ namespace AppInstaller::CLI::Execution // Prompts the user for a path. std::filesystem::path PromptForPath(Resource::LocString message, Level level = Level::Info, std::filesystem::path resultIfDisabled = std::filesystem::path::path()); - // Used to show indefinite progress. Currently an indefinite spinner is the form of - // showing indefinite progress. - // running: shows indefinite progress if set to true, stops indefinite progress if set to false - void ShowIndefiniteProgress(bool running); - // IProgressSink void BeginProgress() override; void OnProgress(uint64_t current, uint64_t maximum, ProgressType type) override; @@ -174,17 +172,28 @@ namespace AppInstaller::CLI::Execution void SetLevelMask(Level reporterLevel, bool setEnabled = true); + // Determines if sixels are supported by the current instance. + bool SixelsSupported(); + + // Determines if sixels are enabled; they must be both supported and enabled by user settings. + bool SixelsEnabled(); + private: Reporter(std::shared_ptr<BaseStream> outStream, std::istream& inStream); // Gets a stream for output for internal use. OutputStream GetBasicOutputStream(); + // Used to show indefinite progress. Currently an indefinite spinner is the form of + // showing indefinite progress. + // running: shows indefinite progress if set to true, stops indefinite progress if set to false + void ShowIndefiniteProgress(bool running); + Channel m_channel = Channel::Output; std::shared_ptr<BaseStream> m_out; std::istream& m_in; std::optional<AppInstaller::Settings::VisualStyle> m_style; - std::optional<IndefiniteSpinner> m_spinner; - std::optional<ProgressBar> m_progressBar; + std::unique_ptr<IIndefiniteSpinner> m_spinner; + std::unique_ptr<IProgressBar> m_progressBar; wil::srwlock m_progressCallbackLock; std::atomic<ProgressCallback*> m_progressCallback; std::atomic<IProgressSink*> m_progressSink; diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp @@ -0,0 +1,711 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Sixel.h" +#include <AppInstallerStrings.h> +#include <winget/UserSettings.h> +#include <vector> +#include <sstream> + +namespace AppInstaller::CLI::VirtualTerminal::Sixel +{ + namespace anon + { + wil::com_ptr<IWICImagingFactory> CreateFactory() + { + wil::com_ptr<IWICImagingFactory> result; + THROW_IF_FAILED(CoCreateInstance( + CLSID_WICImagingFactory, + NULL, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&result))); + return result; + } + + UINT AspectRatioMultiplier(AspectRatio aspectRatio) + { + switch (aspectRatio) + { + case AspectRatio::OneToOne: + return 1; + case AspectRatio::TwoToOne: + return 2; + case AspectRatio::ThreeToOne: + return 3; + case AspectRatio::FiveToOne: + return 5; + default: + THROW_HR(E_INVALIDARG); + } + } + + // Forces the given bitmap source to evaluate + wil::com_ptr<IWICBitmap> CacheToBitmap(IWICImagingFactory* factory, IWICBitmapSource* sourceImage) + { + wil::com_ptr<IWICBitmap> result; + THROW_IF_FAILED(factory->CreateBitmapFromSource(sourceImage, WICBitmapCacheOnLoad, &result)); + return result; + } + + // Convert [0, 255] => [0, 100] + UINT32 ByteToPercent(BYTE input) + { + return (static_cast<UINT32>(input) * 100 + 127) / 255; + } + + // Contains the state for a rendering pass. + struct RenderState + { + RenderState( + const Palette& palette, + const std::vector<ImageView>& views, + const RenderControls& renderControls) : + m_palette(palette), + m_views(views), + m_renderControls(renderControls) + { + // Create render buffers + m_enabledColors.resize(m_palette.Size()); + m_sixelBuffer.resize(m_palette.Size() * m_renderControls.PixelWidth); + } + + enum class State + { + Initial, + Pixels, + Final, + Terminated, + }; + + // Advances the render state machine, returning true if `Current` will return a new sequence and false when it will not. + bool Advance() + { + std::stringstream stream; + + switch (m_currentState) + { + case State::Initial: + // Initial device control string + stream << AICLI_VT_ESCAPE << 'P' << ToIntegral(m_renderControls.AspectRatio) << ";1;q"; + + for (size_t i = 0; i < m_palette.Size(); ++i) + { + // 2 is RGB color space, with values from 0 to 100 + stream << '#' << i << ";2;"; + + WICColor currentColor = m_palette[i]; + BYTE red = (currentColor >> 16) & 0xFF; + BYTE green = (currentColor >> 8) & 0xFF; + BYTE blue = (currentColor) & 0xFF; + + stream << ByteToPercent(red) << ';' << ByteToPercent(green) << ';' << ByteToPercent(blue); + } + + m_currentState = State::Pixels; + break; + case State::Pixels: + { + // Disable all colors and set all characters to empty (0x3F) + memset(m_enabledColors.data(), 0, m_enabledColors.size()); + memset(m_sixelBuffer.data(), 0x3F, m_sixelBuffer.size()); + + // Convert indexed pixel data into per-color sixel lines + UINT rowsToProcess = std::min(RenderControls::PixelsPerSixel, m_renderControls.PixelHeight - m_currentPixelRow); + + for (UINT rowOffset = 0; rowOffset < rowsToProcess; ++rowOffset) + { + // The least significant bit is the top of the sixel + char sixelBit = 1 << rowOffset; + UINT currentRow = m_currentPixelRow + rowOffset; + + for (UINT i = 0; i < m_renderControls.PixelWidth; ++i) + { + const BYTE* pixelPtr = nullptr; + size_t colorIndex = 0; + + for (const ImageView& view : m_views) + { + pixelPtr = view.GetPixel(i, currentRow); + + if (pixelPtr) + { + colorIndex = *pixelPtr; + + // Stop on the first non-transparent pixel we find + if (((m_palette[colorIndex] >> 24) & 0xFF) != 0) + { + break; + } + } + } + + if (pixelPtr) + { + m_enabledColors[colorIndex] = 1; + m_sixelBuffer[(colorIndex * m_renderControls.PixelWidth) + i] += sixelBit; + } + } + } + + // Output all sixel color lines + bool firstOfRow = true; + + for (size_t i = 0; i < m_enabledColors.size(); ++i) + { + if (m_enabledColors[i]) + { + if (m_renderControls.TransparencyEnabled) + { + // Don't output color if transparent + WICColor currentColor = m_palette[i]; + BYTE alpha = (currentColor >> 24) & 0xFF; + if (alpha == 0) + { + continue; + } + } + + if (firstOfRow) + { + firstOfRow = false; + } + else + { + // The carriage return operator resets for another color pass. + stream << '$'; + } + + stream << '#' << i; + + const char* colorRow = &m_sixelBuffer[i * m_renderControls.PixelWidth]; + + if (m_renderControls.UseRepeatSequence) + { + char currentChar = colorRow[0]; + UINT repeatCount = 1; + + for (UINT j = 1; j <= m_renderControls.PixelWidth; ++j) + { + // Force processing of a final null character to handle flushing the line + const char nextChar = (j == m_renderControls.PixelWidth ? 0 : colorRow[j]); + + if (nextChar == currentChar) + { + ++repeatCount; + } + else + { + if (repeatCount > 2) + { + stream << '!' << repeatCount; + } + else if (repeatCount == 2) + { + stream << currentChar; + } + + stream << currentChar; + + currentChar = nextChar; + repeatCount = 1; + } + } + } + else + { + stream << std::string_view{ colorRow, m_renderControls.PixelWidth }; + } + } + } + + // The new line operator sets up for the next sixel row + stream << '-'; + + m_currentPixelRow += rowsToProcess; + if (m_currentPixelRow >= m_renderControls.PixelHeight) + { + m_currentState = State::Final; + } + } + break; + case State::Final: + stream << AICLI_VT_ESCAPE << '\\'; + m_currentState = State::Terminated; + break; + case State::Terminated: + m_currentSequence.clear(); + return false; + } + + m_currentSequence = std::move(stream).str(); + return true; + } + + Sequence Current() const + { + return Sequence{ m_currentSequence }; + } + + private: + const Palette& m_palette; + const std::vector<ImageView>& m_views; + const RenderControls& m_renderControls; + + State m_currentState = State::Initial; + std::vector<char> m_enabledColors; + std::vector<char> m_sixelBuffer; + UINT m_currentPixelRow = 0; + // TODO-C++20: Replace with a view from the stringstream + std::string m_currentSequence; + }; + } + + Palette::Palette(IWICImagingFactory* factory, IWICBitmapSource* bitmapSource, UINT colorCount, bool transparencyEnabled) : + m_factory(factory) + { + THROW_IF_FAILED(m_factory->CreatePalette(&m_paletteObject)); + + THROW_IF_FAILED(m_paletteObject->InitializeFromBitmap(bitmapSource, colorCount, transparencyEnabled)); + + // Extract the palette for render use + UINT actualColorCount = 0; + THROW_IF_FAILED(m_paletteObject->GetColorCount(&actualColorCount)); + + m_palette.resize(actualColorCount); + THROW_IF_FAILED(m_paletteObject->GetColors(actualColorCount, m_palette.data(), &actualColorCount)); + } + + Palette::Palette(const Palette& first, const Palette& second) + { + auto firstPalette = first.m_palette; + auto secondPalette = second.m_palette; + std::sort(firstPalette.begin(), firstPalette.end()); + std::sort(secondPalette.begin(), secondPalette.end()); + + // Construct a union of the two palettes + std::set_union(firstPalette.begin(), firstPalette.end(), secondPalette.begin(), secondPalette.end(), std::back_inserter(m_palette)); + THROW_HR_IF(E_INVALIDARG, m_palette.size() > MaximumColorCount); + + m_factory = first.m_factory; + THROW_IF_FAILED(m_factory->CreatePalette(&m_paletteObject)); + THROW_IF_FAILED(m_paletteObject->InitializeCustom(m_palette.data(), static_cast<UINT>(m_palette.size()))); + } + + IWICPalette* Palette::Get() const + { + return m_paletteObject.get(); + } + + size_t Palette::Size() const + { + return m_palette.size(); + } + + WICColor& Palette::operator[](size_t index) + { + return m_palette[index]; + } + + WICColor Palette::operator[](size_t index) const + { + return m_palette[index]; + } + + ImageView::ImageView(UINT width, UINT height, UINT stride, UINT byteCount, BYTE* bytes) : + m_viewWidth(width), m_viewHeight(height), m_viewStride(stride), m_viewByteCount(byteCount), m_viewBytes(bytes) + {} + + ImageView ImageView::Lock(IWICBitmap* imageSource) + { + WICPixelFormatGUID pixelFormat{}; + THROW_IF_FAILED(imageSource->GetPixelFormat(&pixelFormat)); + THROW_HR_IF(ERROR_INVALID_STATE, GUID_WICPixelFormat8bppIndexed != pixelFormat); + + ImageView result; + + UINT sourceX = 0; + UINT sourceY = 0; + THROW_IF_FAILED(imageSource->GetSize(&sourceX, &sourceY)); + THROW_WIN32_IF(ERROR_BUFFER_OVERFLOW, + sourceX > static_cast<UINT>(std::numeric_limits<INT>::max()) || sourceY > static_cast<UINT>(std::numeric_limits<INT>::max())); + + WICRect rect{}; + rect.Width = static_cast<INT>(sourceX); + rect.Height = static_cast<INT>(sourceY); + + THROW_IF_FAILED(imageSource->Lock(&rect, WICBitmapLockRead, &result.m_lockedImage)); + THROW_IF_FAILED(result.m_lockedImage->GetSize(&result.m_viewWidth, &result.m_viewHeight)); + THROW_IF_FAILED(result.m_lockedImage->GetStride(&result.m_viewStride)); + THROW_IF_FAILED(result.m_lockedImage->GetDataPointer(&result.m_viewByteCount, &result.m_viewBytes)); + + return result; + } + + ImageView ImageView::Copy(IWICBitmapSource* imageSource) + { + WICPixelFormatGUID pixelFormat{}; + THROW_IF_FAILED(imageSource->GetPixelFormat(&pixelFormat)); + THROW_HR_IF(ERROR_INVALID_STATE, GUID_WICPixelFormat8bppIndexed != pixelFormat); + + ImageView result; + + THROW_IF_FAILED(imageSource->GetSize(&result.m_viewWidth, &result.m_viewHeight)); + THROW_WIN32_IF(ERROR_BUFFER_OVERFLOW, + result.m_viewWidth > static_cast<UINT>(std::numeric_limits<INT>::max()) || result.m_viewHeight > static_cast<UINT>(std::numeric_limits<INT>::max())); + + result.m_viewStride = result.m_viewWidth; + result.m_viewByteCount = result.m_viewStride * result.m_viewHeight; + result.m_copiedImage = std::make_unique<BYTE[]>(result.m_viewByteCount); + result.m_viewBytes = result.m_copiedImage.get(); + + THROW_IF_FAILED(imageSource->CopyPixels(nullptr, result.m_viewStride, result.m_viewByteCount, result.m_viewBytes)); + + return result; + } + + void ImageView::Translate(INT x, INT y, bool tile) + { + m_tile = tile; + + if (m_tile) + { + m_translateX = static_cast<UINT>(m_viewWidth - (x % static_cast<INT>(m_viewWidth))); + m_translateY = static_cast<UINT>(m_viewHeight - (y % static_cast<INT>(m_viewHeight))); + } + else + { + m_translateX = static_cast<UINT>(-x); + m_translateY = static_cast<UINT>(-y); + } + } + + const BYTE* ImageView::GetPixel(UINT x, UINT y) const + { + UINT translatedX = x + m_translateX; + UINT tileCountX = translatedX / m_viewWidth; + UINT viewX = translatedX % m_viewWidth; + if (tileCountX && !m_tile) + { + return nullptr; + } + + UINT translatedY = y + m_translateY; + UINT tileCountY = translatedY / m_viewHeight; + UINT viewY = translatedY % m_viewHeight; + if (tileCountY && !m_tile) + { + return nullptr; + } + + return m_viewBytes + (static_cast<size_t>(viewY) * m_viewStride) + viewX; + } + + UINT ImageView::Width() const + { + return m_viewWidth; + } + + UINT ImageView::Height() const + { + return m_viewHeight; + } + + void RenderControls::RenderSizeInCells(UINT width, UINT height) + { + PixelWidth = width * CellWidthInPixels; + + // We don't want to overdraw the row below, so our height must be the largest multiple of 6 that fits in Y cells. + UINT yInPixels = height * CellHeightInPixels; + PixelHeight = yInPixels - (yInPixels % PixelsPerSixel); + } + + ImageSource::ImageSource(const std::filesystem::path& imageFilePath) + { + m_factory = anon::CreateFactory(); + + wil::com_ptr<IWICBitmapDecoder> decoder; + THROW_IF_FAILED(m_factory->CreateDecoderFromFilename(imageFilePath.c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder)); + + wil::com_ptr<IWICBitmapFrameDecode> decodedFrame; + THROW_IF_FAILED(decoder->GetFrame(0, &decodedFrame)); + + m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); + } + + ImageSource::ImageSource(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) + { + m_factory = anon::CreateFactory(); + + wil::com_ptr<IStream> 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<ULONG>(imageBytes.size()), &written)); + THROW_IF_FAILED(stream->Seek({}, STREAM_SEEK_SET, nullptr)); + + wil::com_ptr<IWICBitmapDecoder> decoder; + bool initializeDecoder = true; + + switch (imageEncoding) + { + case Manifest::IconFileTypeEnum::Unknown: + THROW_IF_FAILED(m_factory->CreateDecoderFromStream(stream.get(), NULL, WICDecodeMetadataCacheOnDemand, &decoder)); + initializeDecoder = false; + break; + case Manifest::IconFileTypeEnum::Jpeg: + THROW_IF_FAILED(m_factory->CreateDecoder(GUID_ContainerFormatJpeg, NULL, &decoder)); + break; + case Manifest::IconFileTypeEnum::Png: + THROW_IF_FAILED(m_factory->CreateDecoder(GUID_ContainerFormatPng, NULL, &decoder)); + break; + case Manifest::IconFileTypeEnum::Ico: + THROW_IF_FAILED(m_factory->CreateDecoder(GUID_ContainerFormatIco, NULL, &decoder)); + break; + default: + THROW_HR(E_UNEXPECTED); + } + + if (initializeDecoder) + { + THROW_IF_FAILED(decoder->Initialize(stream.get(), WICDecodeMetadataCacheOnDemand)); + } + + wil::com_ptr<IWICBitmapFrameDecode> decodedFrame; + THROW_IF_FAILED(decoder->GetFrame(0, &decodedFrame)); + + m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); + } + + void ImageSource::Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill, InterpolationMode interpolationMode) + { + if ((pixelWidth && pixelHeight) || targetRenderRatio != AspectRatio::OneToOne) + { + UINT targetX = pixelWidth; + UINT targetY = pixelHeight; + + if (!stretchToFill) + { + // We need to calculate which of the sizes needs to be reduced + UINT sourceImageX = 0; + UINT sourceImageY = 0; + THROW_IF_FAILED(m_sourceImage->GetSize(&sourceImageX, &sourceImageY)); + + double doubleTargetX = targetX; + double doubleTargetY = targetY; + double doubleSourceImageX = sourceImageX; + double doubleSourceImageY = sourceImageY; + + double scaleFactorX = doubleTargetX / doubleSourceImageX; + double targetY_scaledForX = sourceImageY * scaleFactorX; + if (targetY_scaledForX > doubleTargetY) + { + // Scaling to make X fill would make Y to large, so we must scale to fill Y + targetX = static_cast<UINT>(sourceImageX * (doubleTargetY / doubleSourceImageY)); + } + else + { + // Scaling to make X fill kept Y under target + targetY = static_cast<UINT>(targetY_scaledForX); + } + } + + // Apply aspect ratio scaling + targetY /= anon::AspectRatioMultiplier(targetRenderRatio); + + wil::com_ptr<IWICBitmapScaler> scaler; + THROW_IF_FAILED(m_factory->CreateBitmapScaler(&scaler)); + + THROW_IF_FAILED(scaler->Initialize(m_sourceImage.get(), targetX, targetY, ToEnum<WICBitmapInterpolationMode>(ToIntegral(interpolationMode)))); + m_sourceImage = anon::CacheToBitmap(m_factory.get(), scaler.get()); + } + } + + void ImageSource::Resize(const RenderControls& controls) + { + Resize(controls.PixelWidth, controls.PixelHeight, controls.AspectRatio, controls.StretchSourceToFill, controls.InterpolationMode); + } + + Palette ImageSource::CreatePalette(UINT colorCount, bool transparencyEnabled) const + { + return { m_factory.get(), m_sourceImage.get(), colorCount, transparencyEnabled }; + } + + Palette ImageSource::CreatePalette(const RenderControls& controls) const + { + return CreatePalette(controls.ColorCount, controls.TransparencyEnabled); + } + + void ImageSource::ApplyPalette(const Palette& palette) + { + // Convert to 8bpp indexed + wil::com_ptr<IWICFormatConverter> converter; + THROW_IF_FAILED(m_factory->CreateFormatConverter(&converter)); + + // TODO: Determine a better value or enable it to be set + constexpr double s_alphaThreshold = 0.5; + + THROW_IF_FAILED(converter->Initialize(m_sourceImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.Get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); + m_sourceImage = anon::CacheToBitmap(m_factory.get(), converter.get()); + } + + ImageView ImageSource::Lock() const + { + return ImageView::Lock(m_sourceImage.get()); + } + + ImageView ImageSource::Copy() const + { + return ImageView::Copy(m_sourceImage.get()); + } + + void Compositor::Palette(Sixel::Palette palette) + { + m_palette = std::move(palette); + } + + void Compositor::AddView(ImageView&& view) + { + m_views.emplace_back(std::move(view)); + } + + size_t Compositor::ViewCount() const + { + return m_views.size(); + } + + ImageView& Compositor::operator[](size_t index) + { + return m_views[index]; + } + + const ImageView& Compositor::operator[](size_t index) const + { + return m_views[index]; + } + + RenderControls& Compositor::Controls() + { + return m_renderControls; + } + + const RenderControls& Compositor::Controls() const + { + return m_renderControls; + } + + ConstructedSequence Compositor::Render() + { + anon::RenderState renderState{ m_palette, m_views, m_renderControls }; + + std::stringstream result; + + while (renderState.Advance()) + { + result << renderState.Current().Get(); + } + + return ConstructedSequence{ std::move(result).str() }; + } + + void Compositor::RenderTo(Execution::BaseStream& stream) + { + anon::RenderState renderState{ m_palette, m_views, m_renderControls }; + + while (renderState.Advance()) + { + stream << renderState.Current(); + } + } + + void Compositor::RenderTo(Execution::OutputStream& stream) + { + anon::RenderState renderState{ m_palette, m_views, m_renderControls }; + + while (renderState.Advance()) + { + stream << renderState.Current(); + } + } + + Image::Image(const std::filesystem::path& imageFilePath) : + m_imageSource(imageFilePath) + {} + + Image::Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) : + m_imageSource(imageStream, imageEncoding) + {} + + Image& Image::AspectRatio(Sixel::AspectRatio aspectRatio) + { + m_renderControls.AspectRatio = aspectRatio; + return *this; + } + + Image& Image::Transparency(bool transparencyEnabled) + { + m_renderControls.TransparencyEnabled = transparencyEnabled; + return *this; + } + + Image& Image::ColorCount(UINT colorCount) + { + THROW_HR_IF(E_INVALIDARG, colorCount > Palette::MaximumColorCount || colorCount < 2); + m_renderControls.ColorCount = colorCount; + return *this; + } + + Image& Image::RenderSizeInPixels(UINT width, UINT height) + { + m_renderControls.PixelWidth = width; + m_renderControls.PixelHeight = height; + return *this; + } + + Image& Image::RenderSizeInCells(UINT width, UINT height) + { + m_renderControls.RenderSizeInCells(width, height); + return *this; + } + + Image& Image::StretchSourceToFill(bool stretchSourceToFill) + { + m_renderControls.StretchSourceToFill = stretchSourceToFill; + return *this; + } + + Image& Image::UseRepeatSequence(bool useRepeatSequence) + { + m_renderControls.UseRepeatSequence = useRepeatSequence; + return *this; + } + + ConstructedSequence Image::Render() + { + return CreateCompositor().second.Render(); + } + + void Image::RenderTo(Execution::OutputStream& stream) + { + CreateCompositor().second.RenderTo(stream); + } + + std::pair<ImageSource, Compositor> Image::CreateCompositor() + { + ImageSource localSource{ m_imageSource }; + localSource.Resize(m_renderControls); + + Palette palette{ localSource.CreatePalette(m_renderControls) }; + localSource.ApplyPalette(palette); + + ImageView view{ localSource.Lock() }; + + Compositor compositor; + compositor.Palette(std::move(palette)); + compositor.AddView(std::move(view)); + compositor.Controls() = m_renderControls; + + return { std::move(localSource), std::move(compositor) }; + } +} diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ChannelStreams.h" +#include "VTSupport.h" +#include <winget/ManifestLocalization.h> +#include <wincodec.h> +#include <wil/com.h> +#include <filesystem> + +namespace AppInstaller::CLI::VirtualTerminal::Sixel +{ + // Determines the height to width ratio of the pixels that make up a sixel (a sixel is 6 pixels tall and 1 pixel wide). + // Note that each cell is always a height of 20 and a width of 10, regardless of the screen resolution of the terminal. + // The 2:1 ratio will then result in each sixel being 12 of the 20 pixels of a cell. + enum class AspectRatio + { + OneToOne = 7, + TwoToOne = 0, + ThreeToOne = 3, + FiveToOne = 2, + }; + + // Determines the algorithm used when resizing the image. + enum class InterpolationMode + { + NearestNeighbor = WICBitmapInterpolationModeNearestNeighbor, + Linear = WICBitmapInterpolationModeLinear, + Cubic = WICBitmapInterpolationModeCubic, + Fant = WICBitmapInterpolationModeFant, + HighQualityCubic = WICBitmapInterpolationModeHighQualityCubic, + }; + + // Contains the palette used by a sixel image. + struct Palette + { + // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. + static constexpr UINT MaximumColorCount = 256; + + // Creates an empty palette. + Palette() = default; + + // Create a palette from the given source image, color count, transparency setting. + Palette(IWICImagingFactory* factory, IWICBitmapSource* bitmapSource, UINT colorCount, bool transparencyEnabled); + + // Create a palette combining the two palettes. Throws an exception if there are more than MaximumColorCount unique + // colors between the two. This can be avoided by intentionally dividing the available colors between the palettes + // when creating them. + Palette(const Palette& first, const Palette& second); + + // Gets the WIC palette object. + IWICPalette* Get() const; + + // Gets the color count for the palette. + size_t Size() const; + + // Gets the color at the given index in the palette. + WICColor& operator[](size_t index); + WICColor operator[](size_t index) const; + + private: + wil::com_ptr<IWICImagingFactory> m_factory; + wil::com_ptr<IWICPalette> m_paletteObject; + std::vector<WICColor> m_palette; + }; + + // Allows access to the pixel data of an image source. + // Can be configured to translate and/or tile the view. + struct ImageView + { + // Creates a non-owning view using the given data. + ImageView(UINT width, UINT height, UINT stride, UINT byteCount, BYTE* bytes); + + // Create a view by locking a bitmap. + // This must be used from the same thread as the bitmap. + static ImageView Lock(IWICBitmap* imageSource); + + // Create a view by copying the pixels from the image. + static ImageView Copy(IWICBitmapSource* imageSource); + + // Translate the view by the given pixel counts. + // The pixel at [0, 0] of the original will be at [x, y]. + // If tile is true, the view will % coordinates outside of its dimensions back into its own view. + // If tile is false, coordinates outside of the view will be null. + void Translate(INT x, INT y, bool tile); + + // Gets the pixel of the view at the given coordinate. + // Returns null if the coordinate is outside of the view. + const BYTE* GetPixel(UINT x, UINT y) const; + + // Get the dimensions of the view. + UINT Width() const; + UINT Height() const; + + private: + ImageView() = default; + + bool m_tile = false; + UINT m_translateX = 0; + UINT m_translateY = 0; + + wil::com_ptr<IWICBitmapLock> m_lockedImage; + std::unique_ptr<BYTE[]> m_copiedImage; + + UINT m_viewWidth = 0; + UINT m_viewHeight = 0; + UINT m_viewStride = 0; + UINT m_viewByteCount = 0; + BYTE* m_viewBytes = nullptr; + }; + + // The set of values that defines the rendered output. + struct RenderControls + { + // Yes, its right there in the name but the compiler can't read... + static constexpr UINT PixelsPerSixel = 6; + + // Each cell is always a height of 20 and a width of 10, regardless of the screen resolution of the terminal. + static constexpr UINT CellHeightInPixels = 20; + static constexpr UINT CellWidthInPixels = 10; + + Sixel::AspectRatio AspectRatio = AspectRatio::OneToOne; + bool TransparencyEnabled = true; + bool StretchSourceToFill = false; + bool UseRepeatSequence = false; + UINT ColorCount = Palette::MaximumColorCount; + UINT PixelWidth = 0; + UINT PixelHeight = 0; + Sixel::InterpolationMode InterpolationMode = InterpolationMode::HighQualityCubic; + + // The resulting sixel image will render to this size in terminal cells, + // consuming as much as possible of the given size without going over. + void RenderSizeInCells(UINT width, UINT height); + }; + + // Contains an image that can be manipulated and rendered to sixels. + struct ImageSource + { + // Create an image source from a file. + explicit ImageSource(const std::filesystem::path& imageFilePath); + + // Create an empty image source. + ImageSource() = default; + + // Create an image source from a stream. + ImageSource(std::istream& imageStream, 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. + void Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill = false, InterpolationMode interpolationMode = InterpolationMode::HighQualityCubic); + + // Resizes the image using the given render controls. + void Resize(const RenderControls& controls); + + // Creates a palette from the current image. + Palette CreatePalette(UINT colorCount, bool transparencyEnabled) const; + + // Creates a palette from the current image. + Palette CreatePalette(const RenderControls& controls) const; + + // Converts the image to be 8bpp indexed for the given palette. + void ApplyPalette(const Palette& palette); + + // Create a view by locking the image source. + // This must be used from the same thread as the image source. + ImageView Lock() const; + + // Create a view by copying the pixels from the image source. + ImageView Copy() const; + + private: + wil::com_ptr<IWICImagingFactory> m_factory; + wil::com_ptr<IWICBitmap> m_sourceImage; + }; + + // Allows one or more image sources to be rendered to a sixel output. + struct Compositor + { + // Create an empty compositor. + Compositor() = default; + + // Set the palette to be used by the compositor. + void Palette(Palette palette); + + // Adds a new view to the compositor. Each successive view will be behind all of the others. + void AddView(ImageView&& view); + + // Gets the number of views in the compositor. + size_t ViewCount() const; + + // Gets the color at the given index in the palette. + ImageView& operator[](size_t index); + const ImageView& operator[](size_t index) const; + + // Get the render controls for the compositor. + RenderControls& Controls(); + const RenderControls& Controls() const; + + // Render to sixel format for storage / use multiple times. + ConstructedSequence Render(); + + // Renders to sixel format directly to the stream. + void RenderTo(Execution::BaseStream& stream); + + // Renders to sixel format directly to the stream. + void RenderTo(Execution::OutputStream& stream); + + private: + RenderControls m_renderControls; + Sixel::Palette m_palette; + std::vector<ImageView> m_views; + }; + + // A helpful wrapper around the sixel image primitives that makes rendering a single image easier. + struct Image + { + // Create an image from a file. + Image(const std::filesystem::path& imageFilePath); + + // Create an image from a stream. + Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding); + + // Set the aspect ratio of the result. + Image& AspectRatio(AspectRatio aspectRatio); + + // Determine whether transparency is enabled. + // This will affect whether transparent pixels are rendered or not. + Image& Transparency(bool transparencyEnabled); + + // If transparency is enabled, one of the colors will be reserved for it. + Image& ColorCount(UINT colorCount); + + // The resulting sixel image will render to this size in terminal cell pixels. + Image& RenderSizeInPixels(UINT width, UINT height); + + // The resulting sixel image will render to this size in terminal cells, + // consuming as much as possible of the given size without going over. + Image& RenderSizeInCells(UINT width, UINT height); + + // Only affects the scaling of the image that occurs when render size is set. + // When true, the source image will be stretched to fill the target size. + // When false, the source image will be scaled while keeping its original aspect ratio. + Image& StretchSourceToFill(bool stretchSourceToFill); + + // Compresses the output using repeat sequences. + Image& UseRepeatSequence(bool useRepeatSequence); + + // Render to sixel format for storage / use multiple times. + ConstructedSequence Render(); + + // Renders to sixel format directly to the output stream. + void RenderTo(Execution::OutputStream& stream); + + private: + // Creates a compositor for the image using the current render controls. + std::pair<ImageSource, Compositor> CreateCompositor(); + + ImageSource m_imageSource; + RenderControls m_renderControls; + }; +} diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "VTSupport.h" #include <AppInstallerLogging.h> - +#include <AppInstallerStrings.h> namespace AppInstaller::CLI::VirtualTerminal { @@ -17,75 +17,119 @@ namespace AppInstaller::CLI::VirtualTerminal auto color = settings.GetColorValue(UIColorType::Accent); return { color.R, color.G, color.B }; } - } - ConsoleModeRestore::ConsoleModeRestore() - { - // Set output mode to handle virtual terminal sequences - HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (hOut == INVALID_HANDLE_VALUE) - { - LOG_LAST_ERROR(); - } - else if (hOut == NULL) + bool InitializeMode(DWORD handle, DWORD& previousMode, std::initializer_list<DWORD> modeModifierFallbacks, DWORD disabledFlags = 0) { - AICLI_LOG(CLI, Info, << "VT not enabled due to null output handle"); - } - else - { - if (!GetConsoleMode(hOut, &m_previousMode)) + HANDLE hStd = GetStdHandle(handle); + if (hStd == INVALID_HANDLE_VALUE) + { + LOG_LAST_ERROR(); + } + else if (hStd == NULL) { - // If the user redirects output, the handle will be invalid for this function. - // Don't log it in that case. - LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE); + AICLI_LOG(CLI, Info, << "VT not enabled due to null handle [" << handle << "]"); } else { - // Try to degrade in case DISABLE_NEWLINE_AUTO_RETURN isn't supported. - for (DWORD mode : { ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN, ENABLE_VIRTUAL_TERMINAL_PROCESSING}) + if (!GetConsoleMode(hStd, &previousMode)) { - DWORD outMode = m_previousMode | mode; - if (!SetConsoleMode(hOut, outMode)) - { - // Even if it is a different error, log it and try to carry on. - LOG_LAST_ERROR_IF(GetLastError() != STATUS_INVALID_PARAMETER); - } - else + // If the user redirects output, the handle will be invalid for this function. + // Don't log it in that case. + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_HANDLE); + } + else + { + for (DWORD mode : modeModifierFallbacks) { - m_token = true; - break; + DWORD outMode = (previousMode & ~disabledFlags) | mode; + if (!SetConsoleMode(hStd, outMode)) + { + // Even if it is a different error, log it and try to carry on. + LOG_LAST_ERROR_IF(GetLastError() != STATUS_INVALID_PARAMETER); + } + else + { + return true; + } } } } + + return false; + } + + // Extracts a VT sequence, expected one of the form ESCAPE + prefix + result + suffix, returning the result part. + std::string ExtractSequence(std::istream& inStream, std::string_view prefix, std::string_view suffix) + { + std::string result; + + if (inStream.peek() == AICLI_VT_ESCAPE[0]) + { + result.resize(4095); + inStream.readsome(&result[0], result.size()); + THROW_HR_IF(E_UNEXPECTED, static_cast<size_t>(inStream.gcount()) >= result.size()); + + result.resize(static_cast<size_t>(inStream.gcount())); + + std::string_view resultView = result; + size_t overheadLength = 1 + prefix.length() + suffix.length(); + if (resultView.length() <= overheadLength || + resultView.substr(1, prefix.length()) != prefix || + resultView.substr(resultView.length() - suffix.length()) != suffix) + { + result.clear(); + } + else + { + result = result.substr(1 + prefix.length(), result.length() - overheadLength); + } + } + + return result; } } - ConsoleModeRestore::~ConsoleModeRestore() + ConsoleModeRestoreBase::ConsoleModeRestoreBase(DWORD handle) : m_handle(handle) {} + + ConsoleModeRestoreBase::~ConsoleModeRestoreBase() { if (m_token) { - LOG_LAST_ERROR_IF(!SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), m_previousMode)); + LOG_LAST_ERROR_IF(!SetConsoleMode(GetStdHandle(m_handle), m_previousMode)); m_token = false; } } + ConsoleModeRestore::ConsoleModeRestore() : ConsoleModeRestoreBase(STD_OUTPUT_HANDLE) + { + m_token = InitializeMode(STD_OUTPUT_HANDLE, m_previousMode, { ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN, ENABLE_VIRTUAL_TERMINAL_PROCESSING }); + } + const ConsoleModeRestore& ConsoleModeRestore::Instance() { static ConsoleModeRestore s_instance; return s_instance; } + ConsoleInputModeRestore::ConsoleInputModeRestore() : ConsoleModeRestoreBase(STD_INPUT_HANDLE) + { + m_token = InitializeMode(STD_INPUT_HANDLE, m_previousMode, { ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT }, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT); + } + void ConstructedSequence::Append(const Sequence& sequence) { - if (sequence.Get()) + if (!sequence.Get().empty()) { m_str += sequence.Get(); Set(m_str); } } -// The escape character that begins all VT sequences -#define AICLI_VT_ESCAPE "\x1b" + void ConstructedSequence::Clear() + { + m_str.clear(); + Set(m_str); + } // The beginning of a Control Sequence Introducer #define AICLI_VT_CSI AICLI_VT_ESCAPE "[" @@ -93,16 +137,78 @@ namespace AppInstaller::CLI::VirtualTerminal // The beginning of an Operating system command #define AICLI_VT_OSC AICLI_VT_ESCAPE "]" + PrimaryDeviceAttributes::PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream) + { + try + { + ConsoleInputModeRestore inputMode; + if (!inputMode.IsVTEnabled()) + { + return; + } + + // Send DA1 Primary Device Attributes request + outStream << AICLI_VT_CSI << "0c"; + outStream.flush(); + + // Response is of the form AICLI_VT_CSI ? <conformance level> ; (<extension number> ;)* c + std::string sequence = ExtractSequence(inStream, "[?", "c"); + std::vector<std::string> values = Utility::Split(sequence, ';'); + + if (!values.empty()) + { + m_conformanceLevel = std::stoul(values[0]); + } + + for (size_t i = 1; i < values.size(); ++i) + { + m_extensions |= 1ull << std::stoul(values[i]); + } + } + CATCH_LOG(); + } + + bool PrimaryDeviceAttributes::Supports(Extension extension) const + { + uint64_t extensionMask = 1ull << ToIntegral(extension); + return (m_extensions & extensionMask) == extensionMask; + } + namespace Cursor { namespace Position { -#define AICLI_VT_SIMPLE_CURSORPOSITON(_c_) AICLI_VT_ESCAPE #_c_ + ConstructedSequence Up(int16_t cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << AICLI_VT_CSI << cells << 'A'; + return ConstructedSequence{ std::move(result).str() }; + } - const Sequence UpOne{ AICLI_VT_SIMPLE_CURSORPOSITON(A) }; - const Sequence DownOne{ AICLI_VT_SIMPLE_CURSORPOSITON(B) }; - const Sequence ForwardOne{ AICLI_VT_SIMPLE_CURSORPOSITON(C) }; - const Sequence BackwardOne{ AICLI_VT_SIMPLE_CURSORPOSITON(D) }; + ConstructedSequence Down(int16_t cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << AICLI_VT_CSI << cells << 'B'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence Forward(int16_t cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << AICLI_VT_CSI << cells << 'C'; + return ConstructedSequence{ std::move(result).str() }; + } + + ConstructedSequence Backward(int16_t cells) + { + THROW_HR_IF(E_INVALIDARG, cells < 0); + std::ostringstream result; + result << AICLI_VT_CSI << cells << 'D'; + return ConstructedSequence{ std::move(result).str() }; + } } namespace Visibility @@ -145,7 +251,7 @@ namespace AppInstaller::CLI::VirtualTerminal { std::ostringstream result; result << AICLI_VT_CSI "38;2;" << static_cast<uint32_t>(color.R) << ';' << static_cast<uint32_t>(color.G) << ';' << static_cast<uint32_t>(color.B) << 'm'; - return ConstructedSequence{ result.str() }; + return ConstructedSequence{ std::move(result).str() }; } } @@ -155,7 +261,7 @@ namespace AppInstaller::CLI::VirtualTerminal { std::ostringstream result; result << AICLI_VT_CSI "48;2;" << static_cast<uint32_t>(color.R) << ';' << static_cast<uint32_t>(color.G) << ';' << static_cast<uint32_t>(color.B) << 'm'; - return ConstructedSequence{ result.str() }; + return ConstructedSequence{ std::move(result).str() }; } } @@ -163,7 +269,7 @@ namespace AppInstaller::CLI::VirtualTerminal { std::ostringstream result; result << AICLI_VT_OSC "8;;" << ref << AICLI_VT_ESCAPE << "\\" << text << AICLI_VT_OSC << "8;;" << AICLI_VT_ESCAPE << "\\"; - return ConstructedSequence{ result.str() }; + return ConstructedSequence{ std::move(result).str() }; } } @@ -229,7 +335,7 @@ namespace AppInstaller::CLI::VirtualTerminal result << percentage.value(); } result << AICLI_VT_ESCAPE << "\\"; - return ConstructedSequence{ result.str() }; + return ConstructedSequence{ std::move(result).str() }; } } } diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h @@ -7,15 +7,38 @@ #include <iostream> #include <optional> #include <string> +#include <string_view> +// The escape character that begins all VT sequences +#define AICLI_VT_ESCAPE "\x1b" + namespace AppInstaller::CLI::VirtualTerminal { // RAII class to enable VT support and restore the console mode. - struct ConsoleModeRestore + struct ConsoleModeRestoreBase { - ~ConsoleModeRestore(); + ConsoleModeRestoreBase(DWORD handle); + ~ConsoleModeRestoreBase(); + + ConsoleModeRestoreBase(const ConsoleModeRestoreBase&) = delete; + ConsoleModeRestoreBase& operator=(const ConsoleModeRestoreBase&) = delete; + + ConsoleModeRestoreBase(ConsoleModeRestoreBase&&) = default; + ConsoleModeRestoreBase& operator=(ConsoleModeRestoreBase&&) = default; + + // Returns true if VT support has been enabled for the console. + bool IsVTEnabled() const { return m_token; } + + protected: + DestructionToken m_token = false; + DWORD m_handle = 0; + DWORD m_previousMode = 0; + }; + // RAII class to enable VT output support and restore the console mode. + struct ConsoleModeRestore : public ConsoleModeRestoreBase + { ConsoleModeRestore(const ConsoleModeRestore&) = delete; ConsoleModeRestore& operator=(const ConsoleModeRestore&) = delete; @@ -25,29 +48,35 @@ namespace AppInstaller::CLI::VirtualTerminal // Gets the singleton. static const ConsoleModeRestore& Instance(); - // Returns true if VT support has been enabled for the console. - bool IsVTEnabled() const { return m_token; } - private: ConsoleModeRestore(); + }; - DestructionToken m_token = false; - DWORD m_previousMode = 0; + // RAII class to enable VT input support and restore the console mode. + struct ConsoleInputModeRestore : public ConsoleModeRestoreBase + { + ConsoleInputModeRestore(); + + ConsoleInputModeRestore(const ConsoleInputModeRestore&) = delete; + ConsoleInputModeRestore& operator=(const ConsoleInputModeRestore&) = delete; + + ConsoleInputModeRestore(ConsoleInputModeRestore&&) = default; + ConsoleInputModeRestore& operator=(ConsoleInputModeRestore&&) = default; }; // The base for all VT sequences. struct Sequence { - Sequence() = default; - explicit Sequence(const char* c) : m_chars(c) {} + constexpr Sequence() = default; + explicit constexpr Sequence(std::string_view c) : m_chars(c) {} - const char* Get() const { return m_chars; } + std::string_view Get() const { return m_chars; } protected: - void Set(const std::string& s) { m_chars = s.c_str(); } + void Set(const std::string& s) { m_chars = s; } private: - const char* m_chars = nullptr; + std::string_view m_chars; }; // A VT sequence that is constructed at runtime. @@ -57,13 +86,15 @@ namespace AppInstaller::CLI::VirtualTerminal explicit ConstructedSequence(std::string s) : m_str(std::move(s)) { Set(m_str); } ConstructedSequence(const ConstructedSequence& other) : m_str(other.m_str) { Set(m_str); } - ConstructedSequence& operator=(const ConstructedSequence& other) { m_str = other.m_str; Set(m_str); } + ConstructedSequence& operator=(const ConstructedSequence& other) { m_str = other.m_str; Set(m_str); return *this; } ConstructedSequence(ConstructedSequence&& other) noexcept : m_str(std::move(other.m_str)) { Set(m_str); } - ConstructedSequence& operator=(ConstructedSequence&& other) noexcept { m_str = std::move(other.m_str); Set(m_str); } + ConstructedSequence& operator=(ConstructedSequence&& other) noexcept { m_str = std::move(other.m_str); Set(m_str); return *this; } void Append(const Sequence& sequence); + void Clear(); + private: std::string m_str; }; @@ -71,14 +102,54 @@ namespace AppInstaller::CLI::VirtualTerminal // Below are mapped to the sequences described here: // https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + // Contains the response to a DA1 (Primary Device Attributes) request. + struct PrimaryDeviceAttributes + { + // Queries the device attributes on creation. + PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream); + + // The extensions that a device may support. + enum class Extension + { + Columns132 = 1, + PrinterPort = 2, + Sixel = 4, + SelectiveErase = 6, + SoftCharacterSet = 7, + UserDefinedKeys = 8, + NationalReplacementCharacterSets = 9, + Yugoslavian = 12, + EightBitInterface = 14, + TechnicalCharacterSet = 15, + WindowingCapability = 18, + HorizontalScrolling = 21, + ColorText = 22, + Greek = 23, + Turkish = 24, + RectangularAreaOperations = 28, + TextMacros = 32, + ISO_Latin2CharacterSet = 42, + PC_Term = 44, + SoftKeyMap = 45, + ASCII_Emulation = 46, + }; + + // Determines if the given extension is supported. + bool Supports(Extension extension) const; + + private: + uint32_t m_conformanceLevel = 0; + uint64_t m_extensions = 0; + }; + namespace Cursor { namespace Position { - extern const Sequence UpOne; - extern const Sequence DownOne; - extern const Sequence ForwardOne; - extern const Sequence BackwardOne; + ConstructedSequence Up(int16_t cells); + ConstructedSequence Down(int16_t cells); + ConstructedSequence Forward(int16_t cells); + ConstructedSequence Backward(int16_t cells); } namespace Visibility diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -5,11 +5,14 @@ #include "ExecutionContext.h" #include "ManifestComparator.h" #include "PromptFlow.h" +#include "Sixel.h" #include "TableOutput.h" +#include <winget/FileCache.h> #include <winget/ExperimentalFeature.h> #include <winget/ManifestYamlParser.h> #include <winget/Pin.h> #include <winget/PinningData.h> +#include <AppInstallerSHA256.h> #include <winget/Runtime.h> #include <winget/PackageVersionSelection.h> @@ -66,6 +69,77 @@ 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) + { + static constexpr std::array<uint8_t, ToIntegral(Manifest::IconResolutionEnum::Square256) + 1> s_iconResolutionOrder + { + 9, // Unknown + 8, // Custom + 15, // Square16 + 14, // Square20 + 13, // Square24 + 12, // Square30 + 11, // Square32 + 10, // Square36 + 6, // Square40 + 5, // Square48 + 4, // Square60 + 3, // Square64 + 2, // Square72 + 0, // Square80 + 1, // Square96 + 7, // Square256 + }; + + return s_iconResolutionOrder.at(ToIntegral(alternative.Resolution)) < s_iconResolutionOrder.at(ToIntegral(current.Resolution)); + } + + void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try + { + if (!context.Reporter.SixelsEnabled()) + { + return; + } + + auto icons = manifest.CurrentLocalization.Get<Manifest::Localization::Icons>(); + const Manifest::Icon* bestFitIcon = nullptr; + + for (const auto& icon : icons) + { + if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) + { + bestFitIcon = &icon; + } + } + + if (!bestFitIcon) + { + return; + } + + // Use a cache to hold the icons + auto splitUri = Utility::SplitFileNameFromURI(bestFitIcon->Url); + Caching::FileCache fileCache{ Caching::FileCache::Type::Icon, Utility::SHA256::ConvertToString(bestFitIcon->Sha256), { splitUri.first } }; + auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); + + VirtualTerminal::Sixel::Image sixelIcon{ *iconStream, bestFitIcon->FileType }; + + // Using a height of 4 arbitrarily; allow width up to the entire console. + UINT imageHeightCells = 4; + UINT imageWidthCells = static_cast<UINT>(Execution::GetConsoleWidth()); + + sixelIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); + auto infoOut = context.Reporter.Info(); + sixelIcon.RenderTo(infoOut); + + // Force the final sixel line to not be overwritten + infoOut << std::endl; + } + CATCH_LOG(); + Repository::Source OpenNamedSource(Execution::Context& context, Utility::LocIndView sourceName) { Repository::Source source; @@ -1258,12 +1332,14 @@ namespace AppInstaller::CLI::Workflow { const auto& manifest = context.Get<Execution::Data::Manifest>(); ReportIdentity(context, {}, Resource::String::ReportIdentityFound, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id); + ShowManifestIcon(context, manifest); } void ReportManifestIdentityWithVersion::operator()(Execution::Context& context) const { const auto& manifest = context.Get<Execution::Data::Manifest>(); ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id, manifest.Version, m_level); + ShowManifestIcon(context, manifest); } void SelectInstaller(Execution::Context& context) diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -7,6 +7,7 @@ #include <shellapi.h> #include <WinInet.h> #include <msi.h> +#include <wincodec.h> #pragma warning( push ) #pragma warning ( disable : 4458 4100 6031 4702 ) @@ -16,6 +17,7 @@ #include <valijson/adapters/jsoncpp_adapter.hpp> #pragma warning( pop ) +#include <algorithm> #include <array> #include <atomic> #include <csignal> @@ -29,6 +31,7 @@ #include <optional> #include <set> #include <sstream> +#include <string> #include <string_view> #include <vector> diff --git a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj @@ -63,6 +63,9 @@ </AppxManifest> </ItemGroup> <ItemGroup> + <Content Include="Images\progress-sixel\arrow_only.png" /> + <Content Include="Images\progress-sixel\conveyor.png" /> + <Content Include="Images\progress-sixel\folders_only.png" /> <Content Include="Images\SplashScreen.scale-200_altform-colorful_theme-dark.png" /> <Content Include="Images\SplashScreen.scale-200_altform-colorful_theme-light.png" /> <Content Include="Images\SplashScreen.scale-400.png" /> diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png b/src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png Binary files differ. diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png b/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png Binary files differ. diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png b/src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png Binary files differ. diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -326,6 +326,7 @@ <ClCompile Include="Runtime.cpp" /> <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="ShowFlow.cpp" /> + <ClCompile Include="Sixel.cpp" /> <ClCompile Include="SourceFlow.cpp" /> <ClCompile Include="SQLiteDynamicStorage.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -362,6 +362,9 @@ <ClCompile Include="SQLiteDynamicStorage.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="Sixel.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/Sixel.cpp b/src/AppInstallerCLITests/Sixel.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <Sixel.h> + +using namespace AppInstaller::CLI::VirtualTerminal::Sixel; + +void ValidateGetPixel(std::string_view info, UINT_PTR offset, UINT byteCount, UINT_PTR expected) +{ + INFO(info); + REQUIRE(offset < byteCount); + REQUIRE(offset == expected); +} + +TEST_CASE("ImageView_GetPixel", "[sixel]") +{ + UINT width = 20; + UINT height = 20; + UINT stride = 32; + UINT byteCount = height * stride; + BYTE* byteBase = reinterpret_cast<BYTE*>(100); + + ImageView view{ width, height, stride, byteCount, byteBase }; + + ValidateGetPixel("No translation", view.GetPixel(3, 17) - byteBase, byteCount, 17 * stride + 3); + + view.Translate(14, 8, true); + ValidateGetPixel("Positive translation (tile)", view.GetPixel(3, 17) - byteBase, byteCount, 9 * stride + 9); + + view.Translate(-14, 8, true); + ValidateGetPixel("Negative translation (tile)", view.GetPixel(3, 17) - byteBase, byteCount, 9 * stride + 17); + + view.Translate(14, -8, false); + REQUIRE(view.GetPixel(3, 17) == nullptr); + ValidateGetPixel("Negative translation (no tile)", view.GetPixel(15, 1) - byteBase, byteCount, 9 * stride + 1); +} + +TEST_CASE("Image_Render", "[sixel]") +{ + Image image{ TestCommon::TestDataFile("notepad.ico") }; + REQUIRE(!image.Render().Get().empty()); + + image.AspectRatio(AspectRatio::ThreeToOne); + image.ColorCount(64); + image.RenderSizeInCells(2, 1); + image.UseRepeatSequence(true); + REQUIRE(!image.Render().Get().empty()); +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -199,6 +199,20 @@ TEST_CASE("GetFileNameFromURI", "[strings]") REQUIRE(GetFileNameFromURI("https://microsoft.com/").u8string() == ""); } +void ValidateSplitFileName(std::string_view uri, std::string_view base, std::string_view fileName) +{ + auto split = SplitFileNameFromURI(uri); + REQUIRE(split.first == base); + REQUIRE(split.second.u8string() == fileName); +} + +TEST_CASE("SplitFileNameFromURI", "[strings]") +{ + ValidateSplitFileName("https://github.com/microsoft/winget-cli/pull/1722", "https://github.com/microsoft/winget-cli/pull/", "1722"); + ValidateSplitFileName("https://github.com/microsoft/winget-cli/README.md", "https://github.com/microsoft/winget-cli/", "README.md"); + ValidateSplitFileName("https://microsoft.com/", "https://microsoft.com/", ""); +} + TEST_CASE("SplitIntoWords", "[strings]") { REQUIRE(SplitIntoWords("A B") == std::vector<std::string>{ "A", "B" }); diff --git a/src/AppInstallerCommonCore/FileCache.cpp b/src/AppInstallerCommonCore/FileCache.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::Caching case FileCache::Type::IndexV1_Manifest: return "V1_M"; case FileCache::Type::IndexV2_PackageVersionData: return "V2_PVD"; case FileCache::Type::IndexV2_Manifest: return "V2_M"; + case FileCache::Type::Icon: return "Icon"; #ifndef AICLI_DISABLE_TEST_HOOKS case FileCache::Type::Tests: return "Tests"; #endif @@ -55,6 +56,7 @@ namespace AppInstaller::Caching if (!expectedHash.empty() && (!downloadHash || !Utility::SHA256::AreEqual(expectedHash, downloadHash.value()))) { + AICLI_LOG(Core, Verbose, << "Invalid hash from [" << fullPath << "]: expected [" << Utility::SHA256::ConvertToString(expectedHash) << "], got [" << (downloadHash ? Utility::SHA256::ConvertToString(*downloadHash) : "null") << "]"); THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); } @@ -123,6 +125,7 @@ namespace AppInstaller::Caching case Type::IndexV1_Manifest: case Type::IndexV2_PackageVersionData: case Type::IndexV2_Manifest: + case Type::Icon: #ifndef AICLI_DISABLE_TEST_HOOKS case Type::Tests: #endif diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -54,6 +54,8 @@ namespace AppInstaller::Runtime CheckpointsLocation, // The location of the CLI executable file. CLIExecutable, + // The location of the image assets, if it exists. + ImageAssets, // Always one more than the last path; for being able to iterate paths in tests. Max }; diff --git a/src/AppInstallerCommonCore/Public/winget/FileCache.h b/src/AppInstallerCommonCore/Public/winget/FileCache.h @@ -21,6 +21,8 @@ namespace AppInstaller::Caching IndexV2_PackageVersionData, // Manifests for index V2. IndexV2_Manifest, + // Icon for use during show command when sixel rendering is enabled. + Icon, #ifndef AICLI_DISABLE_TEST_HOOKS // The test type. Tests, diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h b/src/AppInstallerCommonCore/Public/winget/ManifestLocalization.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <winget/ManifestCommon.h> #include <AppInstallerStrings.h> #include <variant> @@ -147,4 +148,4 @@ namespace AppInstaller::Manifest private: std::map<Localization, details::LocalizationVariant> m_data; }; -}- \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -43,6 +43,8 @@ namespace AppInstaller::Settings Retro, Accent, Rainbow, + Sixel, + Disabled, }; // The download code to use for *installers*. @@ -65,6 +67,7 @@ namespace AppInstaller::Settings // Visual ProgressBarVisualStyle, AnonymizePathForDisplay, + EnableSixelDisplay, // Source AutoUpdateTimeInMinutes, // Experimental @@ -147,6 +150,7 @@ namespace AppInstaller::Settings // Visual SETTINGMAPPING_SPECIALIZATION(Setting::ProgressBarVisualStyle, std::string, VisualStyle, VisualStyle::Accent, ".visual.progressBar"sv); SETTINGMAPPING_SPECIALIZATION(Setting::AnonymizePathForDisplay, bool, bool, true, ".visual.anonymizeDisplayedPaths"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EnableSixelDisplay, bool, bool, false, ".visual.enableSixels"sv); // Source SETTINGMAPPING_SPECIALIZATION_POLICY(Setting::AutoUpdateTimeInMinutes, uint32_t, std::chrono::minutes, 15min, ".source.autoUpdateIntervalInMinutes"sv, ValuePolicy::SourceAutoUpdateIntervalInMinutes); // Experimental diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -29,6 +29,12 @@ namespace AppInstaller::Runtime constexpr std::string_view s_PortablePackageRoot = "WinGet"sv; constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv; constexpr std::string_view s_LinksDirectory = "Links"sv; +// Use production CLSIDs as a surrogate for repository location. +#if USE_PROD_CLSIDS + constexpr std::string_view s_ImageAssetsDirectoryRelative = "Assets\\WinGet"sv; +#else + constexpr std::string_view s_ImageAssetsDirectoryRelative = "Images"sv; +#endif constexpr std::string_view s_CheckpointsDirectory = "Checkpoints"sv; constexpr std::string_view s_DevModeSubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"sv; constexpr std::string_view s_AllowDevelopmentWithoutDevLicense = "AllowDevelopmentWithoutDevLicense"sv; @@ -311,8 +317,13 @@ namespace AppInstaller::Runtime result = GetPathDetailsCommon(path, forDisplay); break; case PathName::SelfPackageRoot: + case PathName::ImageAssets: result.Path = GetPackagePath(); result.Create = false; + if (path == PathName::ImageAssets) + { + result.Path /= s_ImageAssetsDirectoryRelative; + } break; case PathName::CheckpointsLocation: result = GetPathDetailsForPackagedContext(PathName::LocalState, forDisplay); @@ -411,12 +422,21 @@ namespace AppInstaller::Runtime break; case PathName::SelfPackageRoot: case PathName::CLIExecutable: + case PathName::ImageAssets: result.Path = GetBinaryDirectoryPath(); result.Create = false; if (path == PathName::CLIExecutable) { result.Path /= s_WinGet_Exe; } + else if (path == PathName::ImageAssets) + { + result.Path /= s_ImageAssetsDirectoryRelative; + if (!std::filesystem::is_directory(result.Path)) + { + result.Path.clear(); + } + } break; case PathName::CheckpointsLocation: result = GetPathDetailsForUnpackagedContext(PathName::LocalState, forDisplay); diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -235,27 +235,33 @@ namespace AppInstaller::Settings WINGET_VALIDATE_SIGNATURE(ProgressBarVisualStyle) { - // progressBar property possible values - static constexpr std::string_view s_progressBar_Accent = "accent"; - static constexpr std::string_view s_progressBar_Rainbow = "rainbow"; - static constexpr std::string_view s_progressBar_Retro = "retro"; + std::string lowerValue = ToLower(value); - if (Utility::CaseInsensitiveEquals(value, s_progressBar_Accent)) + if (value == "accent") { return VisualStyle::Accent; } - else if (Utility::CaseInsensitiveEquals(value, s_progressBar_Rainbow)) + else if (value == "rainbow") { return VisualStyle::Rainbow; } - else if (Utility::CaseInsensitiveEquals(value, s_progressBar_Retro)) + else if (value == "retro") { return VisualStyle::Retro; } + else if (value == "sixel") + { + return VisualStyle::Sixel; + } + else if (value == "disabled") + { + return VisualStyle::Disabled; + } return {}; } + WINGET_VALIDATE_PASS_THROUGH(EnableSixelDisplay) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -694,6 +694,12 @@ namespace AppInstaller::Utility return result; } + std::pair<std::string, std::filesystem::path> SplitFileNameFromURI(std::string_view uri) + { + std::filesystem::path filename = GetFileNameFromURI(uri); + return { std::string{ uri.substr(0, uri.size() - filename.u8string().size()) }, filename }; + } + std::filesystem::path GetFileNameFromURI(std::string_view uri) { winrt::Windows::Foundation::Uri winrtUri{ winrt::hstring{ ConvertToUTF16(uri) } }; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -186,6 +186,9 @@ namespace AppInstaller::Utility // Converts the candidate path part into one suitable for the actual file system std::string MakeSuitablePathPart(std::string_view candidate); + // Splits the file name part off of the given URI. + std::pair<std::string, std::filesystem::path> SplitFileNameFromURI(std::string_view uri); + // Gets the file name part of the given URI. std::filesystem::path GetFileNameFromURI(std::string_view uri);