diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 809babdc53..0776f0427d 100644 --- 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 index 9c56b34984..e688eea753 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -234,6 +234,7 @@ ishelp ISQ ISVs itr +IWIC iwr JArray JDictionary @@ -582,8 +583,10 @@ wesome wfsopen wgetenv Whatif +WIC wildcards WINAPI +wincodec windir windowsdeveloper winerror diff --git a/doc/Settings.md b/doc/Settings.md index e79bb04b18..31dbb0ae6d 100644 --- 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 index 86b8c8842e..7cc2001c1d 100644 --- 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 index aafca0d9bf..af3970d3e8 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -389,6 +389,7 @@ + @@ -451,6 +452,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 91c66b0a16..6c072ed000 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -257,6 +257,9 @@ Commands + + Header Files + @@ -484,6 +487,9 @@ Commands + + Source Files + diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp index 4ef60deb5f..b152fd2acd 100644 --- 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 index 9c14550699..4a1d66cdc1 100644 --- 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 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 OutputStream& operator<<(const T& t) { diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index b238e32368..066aec0467 100644 --- 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 #include #include @@ -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(imageWidthCells)); + infoOut << VirtualTerminal::Cursor::Position::Up(static_cast(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 index fcfad4a365..e55b993afe 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -5,6 +5,10 @@ #if _DEBUG #include "DebugCommand.h" #include +#include "AppInstallerDownloader.h" +#include "Sixel.h" + +using namespace AppInstaller::CLI::Execution; namespace AppInstaller::CLI { @@ -55,6 +59,8 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -148,6 +154,204 @@ namespace AppInstaller::CLI " " << 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 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 sixelImagePtr; + + std::string imageUrl{ context.Args.GetArg(WINGET_DEBUG_SIXEL_FILE) }; + + if (Utility::IsUrlRemote(imageUrl)) + { + auto imageStream = std::make_unique(); + ProgressCallback emptyCallback; + Utility::DownloadToStream(imageUrl, *imageStream, Utility::DownloadType::Manifest, emptyCallback); + + sixelImagePtr = std::make_unique(*imageStream, Manifest::IconFileTypeEnum::Unknown); + } + else + { + sixelImagePtr = std::make_unique(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 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 index 7baa28c4e1..5e37520b2d 100644 --- 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 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 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 index 033ef8de16..562e293007 100644 --- 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(message)); + } + + std::shared_ptr 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 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(message)); + ProgressVisualizerBase::SetMessage(message); } - std::shared_ptr ProgressVisualizerBase::Message() + std::shared_ptr 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 m_canceled = false; + std::atomic m_spinnerRunning = false; + std::future 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 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 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 message = this->Message(); - size_t messageLength = message ? Utility::UTF8ColumnWidth(*message) : 0; + private: + std::atomic 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 newMessage = this->Message(); - std::string eraser; - if (newMessage) + double percentage = static_cast(current) / maximum; + size_t blocksOn = static_cast(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(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(current) / maximum; + size_t blocksOn = static_cast(std::floor(percentage * blockWidth)); + size_t partialBlockIndex = static_cast((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(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(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 Message() override { - ShowProgressNoVT(current, maximum, type); + return ProgressVisualizerBase::Message(); } - m_lastCurrent = current; - m_isVisible = true; - } + private: + std::atomic m_canceled = false; + std::atomic m_spinnerRunning = false; + std::future 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 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(current) / maximum; - size_t blocksOn = static_cast(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(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(current) / maximum; - size_t blocksOn = static_cast(std::floor(percentage * blockWidth)); - size_t partialBlockIndex = static_cast((percentage * blockWidth - blocksOn) * 8); - TextFormat::Color accent = TextFormat::Color::GetAccentColor(); + double percentage = static_cast(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((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(percentage * 100) << '%'; + break; + } + + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Normal, static_cast(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 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::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function& sixelSupported) + { + std::unique_ptr result; - switch (type) + switch (style) + { + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: + result = std::make_unique(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(percentage * 100) << '%'; - break; + try + { + result = std::make_unique(stream, enableVT); + } + CATCH_LOG(); } - // Additional VT-based progress reporting, for terminals that support it - m_out << Progress::Construct(Progress::State::Normal, static_cast(percentage * 100)); + if (!result) + { + result = std::make_unique(stream, enableVT, VisualStyle::Accent); + } + break; + case VisualStyle::Disabled: + break; + default: + THROW_HR(E_NOTIMPL); } - else + + return result; + } + + std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function& sixelSupported) + { + std::unique_ptr result; + + switch (style) { - switch (type) + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: + result = std::make_unique(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(stream, enableVT); + } + CATCH_LOG(); } + + if (!result) + { + result = std::make_unique(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 index de49d7e57e..7085ff3154 100644 --- 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 #include #include -#include -#include - -#include -#include -#include +#include #include -#include #include -#include +#include 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 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 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 Message() = 0; - private: - std::atomic m_canceled = false; - std::atomic m_spinnerRunning = false; - std::future 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 CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, const std::function& 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 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 CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, const std::function& sixelSupported); }; } diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp index 85de8e314b..fac4eaf133 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.cpp +++ b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -31,10 +31,12 @@ namespace AppInstaller::CLI::Execution Reporter::Reporter(std::shared_ptr 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 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() && SixelsSupported(); + } +} diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h index 84e7aa6528..6c2047e884 100644 --- 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 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 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 m_out; std::istream& m_in; std::optional m_style; - std::optional m_spinner; - std::optional m_progressBar; + std::unique_ptr m_spinner; + std::unique_ptr m_progressBar; wil::srwlock m_progressCallbackLock; std::atomic m_progressCallback; std::atomic m_progressSink; diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp new file mode 100644 index 0000000000..bae189981c --- /dev/null +++ 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 +#include +#include +#include + +namespace AppInstaller::CLI::VirtualTerminal::Sixel +{ + namespace anon + { + wil::com_ptr CreateFactory() + { + wil::com_ptr 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 CacheToBitmap(IWICImagingFactory* factory, IWICBitmapSource* sourceImage) + { + wil::com_ptr result; + THROW_IF_FAILED(factory->CreateBitmapFromSource(sourceImage, WICBitmapCacheOnLoad, &result)); + return result; + } + + // Convert [0, 255] => [0, 100] + UINT32 ByteToPercent(BYTE input) + { + return (static_cast(input) * 100 + 127) / 255; + } + + // Contains the state for a rendering pass. + struct RenderState + { + RenderState( + const Palette& palette, + const std::vector& 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& m_views; + const RenderControls& m_renderControls; + + State m_currentState = State::Initial; + std::vector m_enabledColors; + std::vector 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(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(std::numeric_limits::max()) || sourceY > static_cast(std::numeric_limits::max())); + + WICRect rect{}; + rect.Width = static_cast(sourceX); + rect.Height = static_cast(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(std::numeric_limits::max()) || result.m_viewHeight > static_cast(std::numeric_limits::max())); + + result.m_viewStride = result.m_viewWidth; + result.m_viewByteCount = result.m_viewStride * result.m_viewHeight; + result.m_copiedImage = std::make_unique(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(m_viewWidth - (x % static_cast(m_viewWidth))); + m_translateY = static_cast(m_viewHeight - (y % static_cast(m_viewHeight))); + } + else + { + m_translateX = static_cast(-x); + m_translateY = static_cast(-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(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 decoder; + THROW_IF_FAILED(m_factory->CreateDecoderFromFilename(imageFilePath.c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder)); + + wil::com_ptr 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 stream; + THROW_IF_FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &stream)); + + auto imageBytes = Utility::ReadEntireStreamAsByteArray(imageStream); + + ULONG written = 0; + THROW_IF_FAILED(stream->Write(imageBytes.data(), static_cast(imageBytes.size()), &written)); + THROW_IF_FAILED(stream->Seek({}, STREAM_SEEK_SET, nullptr)); + + wil::com_ptr 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 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(sourceImageX * (doubleTargetY / doubleSourceImageY)); + } + else + { + // Scaling to make X fill kept Y under target + targetY = static_cast(targetY_scaledForX); + } + } + + // Apply aspect ratio scaling + targetY /= anon::AspectRatioMultiplier(targetRenderRatio); + + wil::com_ptr scaler; + THROW_IF_FAILED(m_factory->CreateBitmapScaler(&scaler)); + + THROW_IF_FAILED(scaler->Initialize(m_sourceImage.get(), targetX, targetY, ToEnum(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 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 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 new file mode 100644 index 0000000000..737dde3830 --- /dev/null +++ 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 +#include +#include +#include + +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 m_factory; + wil::com_ptr m_paletteObject; + std::vector 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 m_lockedImage; + std::unique_ptr 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 m_factory; + wil::com_ptr 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 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 CreateCompositor(); + + ImageSource m_imageSource; + RenderControls m_renderControls; + }; +} diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index 7dc4c5111a..1908497bae 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "VTSupport.h" #include - +#include 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 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(inStream.gcount()) >= result.size()); + + result.resize(static_cast(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 ? ; ( ;)* c + std::string sequence = ExtractSequence(inStream, "[?", "c"); + std::vector 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(color.R) << ';' << static_cast(color.G) << ';' << static_cast(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(color.R) << ';' << static_cast(color.G) << ';' << static_cast(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 index 54934e574f..ed31e7bb56 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -7,15 +7,38 @@ #include #include #include +#include +// 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 index 752862de52..d9510b6a86 100644 --- 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 #include #include #include #include +#include #include #include @@ -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 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(); + 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(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(); ReportIdentity(context, {}, Resource::String::ReportIdentityFound, manifest.CurrentLocalization.Get(), manifest.Id); + ShowManifestIcon(context, manifest); } void ReportManifestIdentityWithVersion::operator()(Execution::Context& context) const { const auto& manifest = context.Get(); ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get(), 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 index e18952c69d..069bd33ad9 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -7,6 +7,7 @@ #include #include #include +#include #pragma warning( push ) #pragma warning ( disable : 4458 4100 6031 4702 ) @@ -16,6 +17,7 @@ #include #pragma warning( pop ) +#include #include #include #include @@ -29,6 +31,7 @@ #include #include #include +#include #include #include diff --git a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj index 10c92b6dd5..7ee094642a 100644 --- a/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj +++ b/src/AppInstallerCLIPackage/AppInstallerCLIPackage.wapproj @@ -63,6 +63,9 @@ + + + diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png b/src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png new file mode 100644 index 0000000000..2042dfe79f Binary files /dev/null and b/src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png differ diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png b/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png new file mode 100644 index 0000000000..c9afd7244d Binary files /dev/null and b/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png differ diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png b/src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png new file mode 100644 index 0000000000..ff8cd88cff Binary files /dev/null and b/src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png differ diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 020a6692ba..9a5990c117 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -326,6 +326,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index e450823d45..8ab353a494 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -359,6 +359,9 @@ Source Files\Repository + + Source Files\CLI + diff --git a/src/AppInstallerCLITests/Sixel.cpp b/src/AppInstallerCLITests/Sixel.cpp new file mode 100644 index 0000000000..b3ae9875d5 --- /dev/null +++ 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 + +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(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 index 68214f4370..01a7e385da 100644 --- 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{ "A", "B" }); diff --git a/src/AppInstallerCommonCore/FileCache.cpp b/src/AppInstallerCommonCore/FileCache.cpp index 1594445d44..23a1ef98ea 100644 --- 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 index edc3127515..2b8a729a2d 100644 --- 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 index 59d1826f3b..82fd2af153 100644 --- 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 index f4e242547b..9e070e047c 100644 --- 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 #include #include @@ -147,4 +148,4 @@ namespace AppInstaller::Manifest private: std::map m_data; }; -} \ No newline at end of file +} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 0abf1ac1c0..5d8615485a 100644 --- 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 index 0474f7eb23..92bb493a83 100644 --- 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 index ea2ef2271a..476971d2f0 100644 --- 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 index df1cf35496..99ba5695bc 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -694,6 +694,12 @@ namespace AppInstaller::Utility return result; } + std::pair 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 index a8b4939244..296789a82f 100644 --- 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 SplitFileNameFromURI(std::string_view uri); + // Gets the file name part of the given URI. std::filesystem::path GetFileNameFromURI(std::string_view uri);