From bde3b077b051db91529d6e0f5614cbe809e5426d Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 5 Sep 2024 12:19:25 -0700 Subject: [PATCH 01/17] Initial sixel interface definition --- .../AppInstallerCLICore.vcxproj | 2 + .../AppInstallerCLICore.vcxproj.filters | 6 ++ src/AppInstallerCLICore/Sixel.cpp | 0 src/AppInstallerCLICore/Sixel.h | 64 +++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 src/AppInstallerCLICore/Sixel.cpp create mode 100644 src/AppInstallerCLICore/Sixel.h 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/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h new file mode 100644 index 0000000000..47b8a3e4a2 --- /dev/null +++ b/src/AppInstallerCLICore/Sixel.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ChannelStreams.h" +#include "VTSupport.h" +#include + +namespace AppInstaller::CLI::VirtualTerminal +{ + // 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 SixelAspectRatio + { + OneToOne = 7, + TwoToOne = 0, + ThreeToOne = 3, + FiveToOne = 2, + }; + + // Contains an image that can be manipulated and rendered to sixels. + struct SixelImage + { + SixelImage(const std::filesystem::path& imageFilePath); + + void AspectRatio(SixelAspectRatio aspectRatio); + void Transparency(bool transparencyEnabled); + + // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. + static constexpr size_t MaximumColorCount = 256; + + // If transparency is enabled, one of the colors will be reserved for it. + void ColorCount(size_t colorCount); + + // The current aspect ratio will be used to convert to cell relative pixel size. + // The resulting sixel image will render to this size in terminal cell pixels. + void RenderSizeInPixels(size_t x, size_t y); + + // The current aspect ratio will be used to convert to cell relative pixel size. + // 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(size_t x, size_t y); + + // 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. + void StretchSourceToFill(bool stretchSourceToFill); + + // 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: + SixelAspectRatio m_aspectRatio = SixelAspectRatio::OneToOne; + bool m_transparencyEnabled = false; + bool m_stretchSourceToFill = false; + + size_t m_colorCount = MaximumColorCount; + size_t m_renderSizeX = 0; + size_t m_renderSizeY = 0; + }; +} From d2773337711e365192f6d501f3ba5114ccec0b76 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 5 Sep 2024 18:05:37 -0700 Subject: [PATCH 02/17] Image class ready to have render state machine implemented --- .github/actions/spelling/allow.txt | 1 + src/AppInstallerCLICore/Sixel.cpp | 211 +++++++++++++++++++++++++++++ src/AppInstallerCLICore/Sixel.h | 45 ++++-- 3 files changed, 243 insertions(+), 14 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 809babdc53..564354c63a 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -302,6 +302,7 @@ silentwithprogress Silverlight simplesave simpletest +sixel sln sqlbuilder sqliteicu diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index e69de29bb2..7d8e3ccbfd 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Sixel.h" +#include + +namespace AppInstaller::CLI::VirtualTerminal +{ + namespace anon + { + UINT AspectRatioMultiplier(SixelAspectRatio aspectRatio) + { + switch (aspectRatio) + { + case SixelAspectRatio::OneToOne: + return 1; + case SixelAspectRatio::TwoToOne: + return 2; + case SixelAspectRatio::ThreeToOne: + return 3; + case SixelAspectRatio::FiveToOne: + return 5; + default: + THROW_HR(E_INVALIDARG); + } + } + + // Contains the state for a rendering pass. + struct RenderState + { + RenderState( + IWICImagingFactory* factory, + wil::com_ptr& sourceImage, + const SixelImage::RenderControls& renderControls) + { + wil::com_ptr currentImage = sourceImage; + + if ((renderControls.SizeX && renderControls.SizeY) || renderControls.AspectRatio != SixelAspectRatio::OneToOne) + { + UINT targetX = renderControls.SizeX; + UINT targetY = renderControls.SizeY; + + if (!renderControls.StretchSourceToFill) + { + // We need to calculate which of the sizes needs to be reduced + UINT sourceImageX = 0; + UINT sourceImageY = 0; + THROW_IF_FAILED(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 = sourceImageX * (doubleTargetY / doubleSourceImageY); + } + else + { + // Scaling to make X fill kept Y under target + targetY = targetY_scaledForX; + } + } + + // Apply aspect ratio scaling + targetY /= AspectRatioMultiplier(renderControls.AspectRatio); + + wil::com_ptr scaler; + THROW_IF_FAILED(factory->CreateBitmapScaler(&scaler)); + + THROW_IF_FAILED(scaler->Initialize(currentImage.get(), targetX, targetY, WICBitmapInterpolationModeHighQualityCubic)); + currentImage = std::move(scaler); + } + + // Force evaluation as we will need to read it at least twice more from here + currentImage = CacheToBitmap(factory, currentImage.get()); + + // Create a color palette + wil::com_ptr palette; + THROW_IF_FAILED(factory->CreatePalette(&palette)); + + THROW_IF_FAILED(palette->InitializeFromBitmap(currentImage.get(), renderControls.ColorCount, renderControls.TransparencyEnabled)); + + // Convert to 8bpp indexed + wil::com_ptr converter; + THROW_IF_FAILED(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(currentImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); + m_source = std::move(converter); + + // Extract the palette for render use + UINT colorCount = 0; + THROW_IF_FAILED(palette->GetColorCount(&colorCount)); + + m_palette.resize(colorCount); + UINT actualColorCount = 0; + THROW_IF_FAILED(palette->GetColors(colorCount, m_palette.data(), &actualColorCount)); + } + + enum class State + { + Initial, + Palette, + 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() + { + + } + + Sequence Current() const + { + return Sequence{ m_currentSequence.c_str() }; + } + + private: + // Forces the given bitmap source to evaluate + static wil::com_ptr CacheToBitmap(IWICImagingFactory* factory, IWICBitmapSource* sourceImage) + { + wil::com_ptr result; + THROW_IF_FAILED(factory->CreateBitmapFromSource(sourceImage, WICBitmapCacheOnLoad, &result)); + return result; + } + + wil::com_ptr m_source; + std::vector m_palette; + State m_currentState = State::Initial; + size_t m_currentPaletteIndex = 0; + size_t m_currentSixel = 0; + std::string m_currentSequence; + }; + } + + SixelImage::SixelImage(const std::filesystem::path& imageFilePath) + { + InitializeFactory(); + + 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 = std::move(decodedFrame); + } + + void SixelImage::AspectRatio(SixelAspectRatio aspectRatio) + { + m_renderControls.AspectRatio = aspectRatio; + } + + void SixelImage::Transparency(bool transparencyEnabled) + { + m_renderControls.TransparencyEnabled = transparencyEnabled; + } + + void SixelImage::ColorCount(UINT colorCount) + { + THROW_HR_IF(E_INVALIDARG, colorCount > MaximumColorCount || colorCount < 2); + m_renderControls.ColorCount = colorCount; + } + + void SixelImage::RenderSizeInPixels(UINT x, UINT y) + { + m_renderControls.SizeX = x; + m_renderControls.SizeY = y; + } + + void SixelImage::RenderSizeInCells(UINT x, UINT y) + { + // 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 = y * CellHeightInPixels; + RenderSizeInPixels(x * CellWidthInPixels, yInPixels - (yInPixels % PixelsPerSixel)); + } + + void SixelImage::StretchSourceToFill(bool stretchSourceToFill) + { + m_renderControls.StretchSourceToFill = stretchSourceToFill; + } + + ConstructedSequence SixelImage::Render() + { + + } + + void SixelImage::RenderTo(Execution::OutputStream& stream) + { + // TODO: Optimize + stream << Render(); + } + + void SixelImage::InitializeFactory() + { + THROW_IF_FAILED(CoCreateInstance( + CLSID_WICImagingFactory, + NULL, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&m_factory))); + } +} diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 47b8a3e4a2..a9ce497cb2 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -3,6 +3,8 @@ #pragma once #include "ChannelStreams.h" #include "VTSupport.h" +#include +#include #include namespace AppInstaller::CLI::VirtualTerminal @@ -21,25 +23,30 @@ namespace AppInstaller::CLI::VirtualTerminal // Contains an image that can be manipulated and rendered to sixels. struct SixelImage { + // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. + static constexpr UINT MaximumColorCount = 256; + + // 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; + SixelImage(const std::filesystem::path& imageFilePath); void AspectRatio(SixelAspectRatio aspectRatio); void Transparency(bool transparencyEnabled); - // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. - static constexpr size_t MaximumColorCount = 256; - // If transparency is enabled, one of the colors will be reserved for it. - void ColorCount(size_t colorCount); + void ColorCount(UINT colorCount); - // The current aspect ratio will be used to convert to cell relative pixel size. // The resulting sixel image will render to this size in terminal cell pixels. - void RenderSizeInPixels(size_t x, size_t y); + void RenderSizeInPixels(UINT x, UINT y); - // The current aspect ratio will be used to convert to cell relative pixel size. // 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(size_t x, size_t y); + void RenderSizeInCells(UINT x, UINT y); // 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. @@ -52,13 +59,23 @@ namespace AppInstaller::CLI::VirtualTerminal // Renders to sixel format directly to the output stream. void RenderTo(Execution::OutputStream& stream); + // The set of values that defines the rendered output. + struct RenderControls + { + SixelAspectRatio AspectRatio = SixelAspectRatio::OneToOne; + bool TransparencyEnabled = false; + bool StretchSourceToFill = false; + UINT ColorCount = MaximumColorCount; + UINT SizeX = 0; + UINT SizeY = 0; + }; + private: - SixelAspectRatio m_aspectRatio = SixelAspectRatio::OneToOne; - bool m_transparencyEnabled = false; - bool m_stretchSourceToFill = false; + void InitializeFactory(); + + wil::com_ptr m_factory; + wil::com_ptr m_sourceImage; - size_t m_colorCount = MaximumColorCount; - size_t m_renderSizeX = 0; - size_t m_renderSizeY = 0; + RenderControls m_renderControls; }; } From 85d26352c69194ae45969a5f52d51cf412427d7b Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 6 Sep 2024 10:45:32 -0700 Subject: [PATCH 03/17] Only need to render --- .github/actions/spelling/allow.txt | 1 + .github/actions/spelling/expect.txt | 3 + src/AppInstallerCLICore/Sixel.cpp | 97 ++++++++++++++++++--------- src/AppInstallerCLICore/VTSupport.cpp | 2 +- src/AppInstallerCLICore/VTSupport.h | 11 +-- 5 files changed, 76 insertions(+), 38 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 564354c63a..0776f0427d 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -303,6 +303,7 @@ 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/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index 7d8e3ccbfd..2c5fb6fd38 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Sixel.h" #include +#include namespace AppInstaller::CLI::VirtualTerminal { @@ -25,6 +26,14 @@ namespace AppInstaller::CLI::VirtualTerminal } } + // 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; + } + // Contains the state for a rendering pass. struct RenderState { @@ -45,7 +54,7 @@ namespace AppInstaller::CLI::VirtualTerminal // We need to calculate which of the sizes needs to be reduced UINT sourceImageX = 0; UINT sourceImageY = 0; - THROW_IF_FAILED(sourceImage->GetSize(&sourceImageX, &sourceImageY)); + THROW_IF_FAILED(currentImage->GetSize(&sourceImageX, &sourceImageY)); double doubleTargetX = targetX; double doubleTargetY = targetY; @@ -57,12 +66,12 @@ namespace AppInstaller::CLI::VirtualTerminal if (targetY_scaledForX > doubleTargetY) { // Scaling to make X fill would make Y to large, so we must scale to fill Y - targetX = sourceImageX * (doubleTargetY / doubleSourceImageY); + targetX = static_cast(sourceImageX * (doubleTargetY / doubleSourceImageY)); } else { // Scaling to make X fill kept Y under target - targetY = targetY_scaledForX; + targetY = static_cast(targetY_scaledForX); } } @@ -73,27 +82,17 @@ namespace AppInstaller::CLI::VirtualTerminal THROW_IF_FAILED(factory->CreateBitmapScaler(&scaler)); THROW_IF_FAILED(scaler->Initialize(currentImage.get(), targetX, targetY, WICBitmapInterpolationModeHighQualityCubic)); - currentImage = std::move(scaler); + currentImage = CacheToBitmap(factory, scaler.get()); } - // Force evaluation as we will need to read it at least twice more from here - currentImage = CacheToBitmap(factory, currentImage.get()); - // Create a color palette wil::com_ptr palette; THROW_IF_FAILED(factory->CreatePalette(&palette)); THROW_IF_FAILED(palette->InitializeFromBitmap(currentImage.get(), renderControls.ColorCount, renderControls.TransparencyEnabled)); - // Convert to 8bpp indexed - wil::com_ptr converter; - THROW_IF_FAILED(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(currentImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); - m_source = std::move(converter); + // TODO: Determine if the transparent color is always at index 0 + // If not, we should swap it to 0 before conversion to indexed // Extract the palette for render use UINT colorCount = 0; @@ -102,12 +101,21 @@ namespace AppInstaller::CLI::VirtualTerminal m_palette.resize(colorCount); UINT actualColorCount = 0; THROW_IF_FAILED(palette->GetColors(colorCount, m_palette.data(), &actualColorCount)); + + // Convert to 8bpp indexed + wil::com_ptr converter; + THROW_IF_FAILED(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(currentImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); + m_source = CacheToBitmap(factory, converter.get()); } enum class State { Initial, - Palette, Pixels, Final, Terminated, @@ -116,28 +124,40 @@ namespace AppInstaller::CLI::VirtualTerminal // Advances the render state machine, returning true if `Current` will return a new sequence and false when it will not. bool Advance() { + // TODO: See if we can keep a stringstream around to reuse its memory + switch (m_currentState) + { + case State::Initial: + // TODO: Output sixel initialization sequence and palette + m_currentState = State::Pixels; + break; + case State::Pixels: + // TODO: Output a row of sixels + break; + case State::Final: + // TODO: Ouput the sixel termination sequence + m_currentState = State::Terminated; + break; + case State::Terminated: + m_currentSequence.clear(); + return false; + } + + return true; } Sequence Current() const { - return Sequence{ m_currentSequence.c_str() }; + return Sequence{ m_currentSequence }; } private: - // Forces the given bitmap source to evaluate - static wil::com_ptr CacheToBitmap(IWICImagingFactory* factory, IWICBitmapSource* sourceImage) - { - wil::com_ptr result; - THROW_IF_FAILED(factory->CreateBitmapFromSource(sourceImage, WICBitmapCacheOnLoad, &result)); - return result; - } - - wil::com_ptr m_source; + wil::com_ptr m_source; std::vector m_palette; State m_currentState = State::Initial; - size_t m_currentPaletteIndex = 0; - size_t m_currentSixel = 0; + size_t m_currentSixelRow = 0; + // TODO-C++20: Replace with a view from the stringstream std::string m_currentSequence; }; } @@ -152,7 +172,7 @@ namespace AppInstaller::CLI::VirtualTerminal wil::com_ptr decodedFrame; THROW_IF_FAILED(decoder->GetFrame(0, &decodedFrame)); - m_sourceImage = std::move(decodedFrame); + m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } void SixelImage::AspectRatio(SixelAspectRatio aspectRatio) @@ -191,13 +211,26 @@ namespace AppInstaller::CLI::VirtualTerminal ConstructedSequence SixelImage::Render() { + anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; + + std::stringstream result; + + while (renderState.Advance()) + { + result << renderState.Current().Get(); + } + return ConstructedSequence{ std::move(result).str() }; } void SixelImage::RenderTo(Execution::OutputStream& stream) { - // TODO: Optimize - stream << Render(); + anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; + + while (renderState.Advance()) + { + stream << renderState.Current(); + } } void SixelImage::InitializeFactory() diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index 7dc4c5111a..5176b8febf 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -77,7 +77,7 @@ namespace AppInstaller::CLI::VirtualTerminal void ConstructedSequence::Append(const Sequence& sequence) { - if (sequence.Get()) + if (!sequence.Get().empty()) { m_str += sequence.Get(); Set(m_str); diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index 54934e574f..30f8aba470 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -7,6 +7,7 @@ #include #include #include +#include namespace AppInstaller::CLI::VirtualTerminal @@ -38,16 +39,16 @@ namespace AppInstaller::CLI::VirtualTerminal // 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. From f5f2a9497b561d2b724cbf754d89490b71a391c8 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 6 Sep 2024 17:47:00 -0700 Subject: [PATCH 04/17] renderer implemented and debug command added --- src/AppInstallerCLICore/ChannelStreams.cpp | 7 +- src/AppInstallerCLICore/ChannelStreams.h | 3 + .../Commands/DebugCommand.cpp | 84 +++++++++ .../Commands/DebugCommand.h | 14 ++ src/AppInstallerCLICore/ExecutionReporter.cpp | 2 +- src/AppInstallerCLICore/Sixel.cpp | 175 +++++++++++++++++- src/AppInstallerCLICore/Sixel.h | 4 + src/AppInstallerCLICore/VTSupport.cpp | 7 +- src/AppInstallerCLICore/VTSupport.h | 5 + 9 files changed, 291 insertions(+), 10 deletions(-) diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp index 4ef60deb5f..5c1a9f4c5e 100644 --- a/src/AppInstallerCLICore/ChannelStreams.cpp +++ b/src/AppInstallerCLICore/ChannelStreams.cpp @@ -82,6 +82,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 +157,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..cc216eefb2 100644 --- a/src/AppInstallerCLICore/ChannelStreams.h +++ b/src/AppInstallerCLICore/ChannelStreams.h @@ -60,6 +60,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/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index fcfad4a365..845e3097e4 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -5,6 +5,9 @@ #if _DEBUG #include "DebugCommand.h" #include +#include "Sixel.h" + +using namespace AppInstaller::CLI::Execution; namespace AppInstaller::CLI { @@ -55,6 +58,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -148,6 +152,86 @@ namespace AppInstaller::CLI " " << std::endl; } } + + std::vector ShowSixelCommand::GetArguments() const + { + return { + Argument{ "file", 'f', Args::Type::Manifest, Resource::String::SourceListUpdatedNever, ArgumentType::Positional }, + Argument{ "aspect-ratio", 'a', Args::Type::AcceptPackageAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "transparent", 't', Args::Type::AcceptSourceAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "color-count", 'c', Args::Type::ConfigurationAcceptWarning, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "width", 'w', Args::Type::AdminSettingEnable, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "height", 'h', Args::Type::AllowReboot, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "stretch", 's', Args::Type::AllVersions, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "repeat", 'r', Args::Type::Name, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "out-file", 'o', Args::Type::BlockingPin, 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; + SixelImage sixelImage{ Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::Manifest)) }; + + if (context.Args.Contains(Args::Type::AcceptPackageAgreements)) + { + switch (context.Args.GetArg(Args::Type::AcceptPackageAgreements)[0]) + { + case '1': + sixelImage.AspectRatio(SixelAspectRatio::OneToOne); + break; + case '2': + sixelImage.AspectRatio(SixelAspectRatio::TwoToOne); + break; + case '3': + sixelImage.AspectRatio(SixelAspectRatio::ThreeToOne); + break; + case '5': + sixelImage.AspectRatio(SixelAspectRatio::FiveToOne); + break; + } + } + + sixelImage.Transparency(context.Args.Contains(Args::Type::AcceptSourceAgreements)); + + if (context.Args.Contains(Args::Type::ConfigurationAcceptWarning)) + { + sixelImage.ColorCount(std::stoul(std::string{ context.Args.GetArg(Args::Type::ConfigurationAcceptWarning) })); + } + + if (context.Args.Contains(Args::Type::AdminSettingEnable) && context.Args.Contains(Args::Type::AllowReboot)) + { + sixelImage.RenderSizeInCells( + std::stoul(std::string{ context.Args.GetArg(Args::Type::AdminSettingEnable) }), + std::stoul(std::string{ context.Args.GetArg(Args::Type::AllowReboot) })); + } + + sixelImage.StretchSourceToFill(context.Args.Contains(Args::Type::AllVersions)); + + sixelImage.UseRepeatSequence(context.Args.Contains(Args::Type::Name)); + + if (context.Args.Contains(Args::Type::BlockingPin)) + { + std::ofstream stream{ Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::BlockingPin)) }; + stream << sixelImage.Render().Get(); + } + else + { + OutputStream stream = context.Reporter.GetOutputStream(Reporter::Level::Info); + stream.ClearFormat(); + sixelImage.RenderTo(stream); + } + } } #endif diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.h b/src/AppInstallerCLICore/Commands/DebugCommand.h index 7baa28c4e1..36fd05c09f 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.h +++ b/src/AppInstallerCLICore/Commands/DebugCommand.h @@ -57,6 +57,20 @@ 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; + }; } #endif diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp index 85de8e314b..8c48a70f3f 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.cpp +++ b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -353,4 +353,4 @@ namespace AppInstaller::CLI::Execution WI_ClearAllFlags(m_enabledLevels, reporterLevel); } } -} \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index 2c5fb6fd38..f4a744daed 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -34,13 +34,24 @@ namespace AppInstaller::CLI::VirtualTerminal return result; } + // Convert [0, 255] => [0, 100] + UINT32 ByteToPercent(BYTE input) + { + UINT32 result = static_cast(input); + result *= 100; + UINT32 fractional = result % 255; + result /= 255; + return result + (fractional >= 128 ? 1 : 0); + } + // Contains the state for a rendering pass. struct RenderState { RenderState( IWICImagingFactory* factory, wil::com_ptr& sourceImage, - const SixelImage::RenderControls& renderControls) + const SixelImage::RenderControls& renderControls) : + m_renderControls(renderControls) { wil::com_ptr currentImage = sourceImage; @@ -111,6 +122,26 @@ namespace AppInstaller::CLI::VirtualTerminal THROW_IF_FAILED(converter->Initialize(currentImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); m_source = CacheToBitmap(factory, converter.get()); + + // Lock the image for rendering + UINT sourceX = 0; + UINT sourceY = 0; + THROW_IF_FAILED(currentImage->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(m_source->Lock(&rect, WICBitmapLockRead, &m_lockedSource)); + THROW_IF_FAILED(m_lockedSource->GetSize(&m_lockedImageWidth, &m_lockedImageHeight)); + THROW_IF_FAILED(m_lockedSource->GetStride(&m_lockedImageStride)); + THROW_IF_FAILED(m_lockedSource->GetDataPointer(&m_lockedImageByteCount, &m_lockedImageBytes)); + + // Create render buffers + m_enabledColors.resize(m_palette.size()); + m_sixelBuffer.resize(m_palette.size() * m_lockedImageWidth); } enum class State @@ -124,19 +155,134 @@ namespace AppInstaller::CLI::VirtualTerminal // Advances the render state machine, returning true if `Current` will return a new sequence and false when it will not. bool Advance() { - // TODO: See if we can keep a stringstream around to reuse its memory + std::stringstream stream; switch (m_currentState) { case State::Initial: - // TODO: Output sixel initialization sequence and palette + // Initial device control string + stream << AICLI_VT_ESCAPE << 'P' << ToIntegral(m_renderControls.AspectRatio) << ';' << (m_renderControls.TransparencyEnabled ? '1' : '0') << ";q"; + + for (size_t i = 0; i < m_palette.size(); ++i) + { + // 2 is RGB colorspace, 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: - // TODO: Output a row of sixels + { + // 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(SixelImage::PixelsPerSixel, m_lockedImageHeight - m_currentPixelRow); + size_t imageStride = static_cast(m_lockedImageStride); + size_t imageWidth = static_cast(m_lockedImageWidth); + const BYTE* currentRowPtr = m_lockedImageBytes + (imageStride * m_currentPixelRow); + + for (UINT rowOffset = 0; rowOffset < rowsToProcess; ++rowOffset) + { + // The least significant bit is the top of the sixel + char sixelBit = 1 << rowOffset; + + for (size_t i = 0; i < imageWidth; ++i) + { + BYTE colorIndex = currentRowPtr[i]; + m_enabledColors[colorIndex] = 1; + m_sixelBuffer[(colorIndex * imageWidth) + i] += sixelBit; + } + + currentRowPtr += imageStride; + } + + // Output all sixel color lines + bool firstOfRow = true; + + for (size_t i = 0; i < m_enabledColors.size(); ++i) + { + // Don't output color 0 if transparency is enabled + if (m_renderControls.TransparencyEnabled && i == 0) + { + continue; + } + + if (m_enabledColors[i]) + { + if (firstOfRow) + { + firstOfRow = false; + } + else + { + // The carriage return operator resets for another color pass. + stream << '$'; + } + + stream << '#' << i; + + const char* colorRow = &m_sixelBuffer[i * imageWidth]; + + if (m_renderControls.UseRepeatSequence) + { + char currentChar = colorRow[0]; + UINT repeatCount = 1; + + for (size_t j = 1; j <= imageWidth; ++j) + { + // Force processing of a final null character to handle flushing the line + const char nextChar = (j == imageWidth ? 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, imageWidth }; + } + } + } + + // The new line operator sets up for the next sixel row + stream << '-'; + + m_currentPixelRow += rowsToProcess; + if (m_currentPixelRow >= m_lockedImageHeight) + { + m_currentState = State::Final; + } + } break; case State::Final: - // TODO: Ouput the sixel termination sequence + stream << AICLI_VT_ESCAPE << '\\'; m_currentState = State::Terminated; break; case State::Terminated: @@ -144,6 +290,7 @@ namespace AppInstaller::CLI::VirtualTerminal return false; } + m_currentSequence = std::move(stream).str(); return true; } @@ -154,9 +301,20 @@ namespace AppInstaller::CLI::VirtualTerminal private: wil::com_ptr m_source; + wil::com_ptr m_lockedSource; std::vector m_palette; + SixelImage::RenderControls m_renderControls; + + UINT m_lockedImageWidth = 0; + UINT m_lockedImageHeight = 0; + UINT m_lockedImageStride = 0; + UINT m_lockedImageByteCount = 0; + BYTE* m_lockedImageBytes = nullptr; + State m_currentState = State::Initial; - size_t m_currentSixelRow = 0; + 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; }; @@ -209,6 +367,11 @@ namespace AppInstaller::CLI::VirtualTerminal m_renderControls.StretchSourceToFill = stretchSourceToFill; } + void SixelImage::UseRepeatSequence(bool useRepeatSequence) + { + m_renderControls.UseRepeatSequence = useRepeatSequence; + } + ConstructedSequence SixelImage::Render() { anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index a9ce497cb2..2aac3e9b7b 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -53,6 +53,9 @@ namespace AppInstaller::CLI::VirtualTerminal // When false, the source image will be scaled while keeping its original aspect ratio. void StretchSourceToFill(bool stretchSourceToFill); + // Compresses the output using repeat sequences. + void UseRepeatSequence(bool useRepeatSequence); + // Render to sixel format for storage / use multiple times. ConstructedSequence Render(); @@ -65,6 +68,7 @@ namespace AppInstaller::CLI::VirtualTerminal SixelAspectRatio AspectRatio = SixelAspectRatio::OneToOne; bool TransparencyEnabled = false; bool StretchSourceToFill = false; + bool UseRepeatSequence = false; UINT ColorCount = MaximumColorCount; UINT SizeX = 0; UINT SizeY = 0; diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index 5176b8febf..d16fec7184 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -84,8 +84,11 @@ namespace AppInstaller::CLI::VirtualTerminal } } -// 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 "[" diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index 30f8aba470..a062eb6d7c 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -10,6 +10,9 @@ #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. @@ -65,6 +68,8 @@ namespace AppInstaller::CLI::VirtualTerminal void Append(const Sequence& sequence); + void Clear(); + private: std::string m_str; }; From aa8a77dc909eed49ee5db7805da1dd20fc2d9222 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 16 Sep 2024 10:39:26 -0700 Subject: [PATCH 05/17] Improve transparency handling --- src/AppInstallerCLICore/Commands/DebugCommand.cpp | 3 +++ src/AppInstallerCLICore/Sixel.cpp | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index 845e3097e4..31fd3df970 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -230,6 +230,9 @@ namespace AppInstaller::CLI OutputStream stream = context.Reporter.GetOutputStream(Reporter::Level::Info); stream.ClearFormat(); sixelImage.RenderTo(stream); + + // Force a new line to show entire image + stream << std::endl; } } } diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index f4a744daed..c3723b93df 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -161,7 +161,7 @@ namespace AppInstaller::CLI::VirtualTerminal { case State::Initial: // Initial device control string - stream << AICLI_VT_ESCAPE << 'P' << ToIntegral(m_renderControls.AspectRatio) << ';' << (m_renderControls.TransparencyEnabled ? '1' : '0') << ";q"; + stream << AICLI_VT_ESCAPE << 'P' << ToIntegral(m_renderControls.AspectRatio) << ";1;q"; for (size_t i = 0; i < m_palette.size(); ++i) { @@ -210,8 +210,10 @@ namespace AppInstaller::CLI::VirtualTerminal for (size_t i = 0; i < m_enabledColors.size(); ++i) { - // Don't output color 0 if transparency is enabled - if (m_renderControls.TransparencyEnabled && i == 0) + // Don't output color if transparent + WICColor currentColor = m_palette[i]; + BYTE alpha = (currentColor >> 24) & 0xFF; + if (alpha == 0) { continue; } From fa9d1b06c9e05ed24dcb5fe433f866db734f2521 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 16 Sep 2024 17:18:48 -0700 Subject: [PATCH 06/17] Add sixel setting; winget icon; icon during show command --- doc/Settings.md | 668 +++++++++--------- .../JSON/settings/settings.schema.0.2.json | 5 + src/AppInstallerCLICore/Command.cpp | 27 +- .../Commands/DebugCommand.cpp | 20 +- src/AppInstallerCLICore/Sixel.cpp | 55 ++ src/AppInstallerCLICore/Sixel.h | 7 +- src/AppInstallerCLICore/VTSupport.cpp | 43 +- src/AppInstallerCLICore/VTSupport.h | 12 +- .../Workflows/WorkflowBase.cpp | 76 ++ src/AppInstallerCommonCore/FileCache.cpp | 3 + .../Public/AppInstallerRuntime.h | 2 + .../Public/winget/FileCache.h | 2 + .../Public/winget/ManifestLocalization.h | 3 +- .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/Runtime.cpp | 17 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + .../AppInstallerStrings.cpp | 6 + .../Public/AppInstallerStrings.h | 3 + 18 files changed, 604 insertions(+), 348 deletions(-) diff --git a/doc/Settings.md b/doc/Settings.md index e79bb04b18..309d09710e 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -1,335 +1,345 @@ -# WinGet CLI Settings - -You can configure WinGet by editing the `settings.json` file. Running `winget settings` will open the file in the default json editor; if no editor is configured, Windows will prompt for you to select an editor, and Notepad is a sensible option if you have no other preference. - -## File Location - -Settings file is located in %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json - -If you are using the non-packaged WinGet version by building it from source code, the file will be located under %LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json - -## Source - -The `source` settings involve configuration to the WinGet source. - -```json - "source": { - "autoUpdateIntervalInMinutes": 3 - }, -``` - -### autoUpdateIntervalInMinutes - -A positive integer represents the update interval in minutes. The check for updates only happens when a source is used. A zero will disable the check for updates to a source. Any other values are invalid. - -- Disable: 0 -- Default: 5 - -To manually update the source use `winget source update` - -## Visual - -The `visual` settings involve visual elements that are displayed by WinGet - -### progressBar - -Color of the progress bar that WinGet displays when not specified by arguments. - -- accent (default) -- retro -- rainbow - -```json - "visual": { - "progressBar": "accent" - }, -``` - -### anonymizeDisplayedPaths - -Replaces some known folder paths with their respective environment variable. Defaults to true. - -```json - "visual": { - "anonymizeDisplayedPaths": true - }, -``` - -## Install Behavior - -The `installBehavior` settings affect the default behavior of installing and upgrading (where applicable) packages. - -### Disable Install Notes -The `disableInstallNotes` behavior affects whether installation notes are shown after a successful install. Defaults to `false` if value is not set or is invalid. - -```json - "installBehavior": { - "disableInstallNotes": true - }, -``` - -### Portable Package User Root -The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. - -> Note: This setting value must be an absolute path. - -```json - "installBehavior": { - "portablePackageUserRoot": "C:/Users/FooBar/Packages" - }, -``` - -### Portable Package Machine Root -The `portablePackageMachineRoot` setting affects the default root directory where packages are installed to under `Machine` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%PROGRAMFILES%/WinGet/Packages/` if value is not set or is invalid. - -> Note: This setting value must be an absolute path. - -```json - "installBehavior": { - "portablePackageMachineRoot": "C:/Program Files/Packages/Portable" - }, -``` - -### Skip Dependencies -The 'skipDependencies' behavior affects whether dependencies are installed for a given package. Defaults to 'false' if value is not set or is invalid. - -```json - "installBehavior": { - "skipDependencies": true - }, +# WinGet CLI Settings + +You can configure WinGet by editing the `settings.json` file. Running `winget settings` will open the file in the default json editor; if no editor is configured, Windows will prompt for you to select an editor, and Notepad is a sensible option if you have no other preference. + +## File Location + +Settings file is located in %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json + +If you are using the non-packaged WinGet version by building it from source code, the file will be located under %LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json + +## Source + +The `source` settings involve configuration to the WinGet source. + +```json + "source": { + "autoUpdateIntervalInMinutes": 3 + }, +``` + +### autoUpdateIntervalInMinutes + +A positive integer represents the update interval in minutes. The check for updates only happens when a source is used. A zero will disable the check for updates to a source. Any other values are invalid. + +- Disable: 0 +- Default: 5 + +To manually update the source use `winget source update` + +## Visual + +The `visual` settings involve visual elements that are displayed by WinGet + +### progressBar + +Color of the progress bar that WinGet displays when not specified by arguments. + +- accent (default) +- retro +- rainbow + +```json + "visual": { + "progressBar": "accent" + }, +``` + +### anonymizeDisplayedPaths + +Replaces some known folder paths with their respective environment variable. Defaults to true. + +```json + "visual": { + "anonymizeDisplayedPaths": true + }, +``` + +### 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. + +### Disable Install Notes +The `disableInstallNotes` behavior affects whether installation notes are shown after a successful install. Defaults to `false` if value is not set or is invalid. + +```json + "installBehavior": { + "disableInstallNotes": true + }, +``` + +### Portable Package User Root +The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. + +> Note: This setting value must be an absolute path. + +```json + "installBehavior": { + "portablePackageUserRoot": "C:/Users/FooBar/Packages" + }, +``` + +### Portable Package Machine Root +The `portablePackageMachineRoot` setting affects the default root directory where packages are installed to under `Machine` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%PROGRAMFILES%/WinGet/Packages/` if value is not set or is invalid. + +> Note: This setting value must be an absolute path. + +```json + "installBehavior": { + "portablePackageMachineRoot": "C:/Program Files/Packages/Portable" + }, +``` + +### Skip Dependencies +The 'skipDependencies' behavior affects whether dependencies are installed for a given package. Defaults to 'false' if value is not set or is invalid. + +```json + "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. -```json - "installBehavior": { - "archiveExtractionMethod": "tar" | "shellApi" - }, -``` - -### Preferences and Requirements - -Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. - -Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. - -### Scope - -The `scope` behavior affects the choice between installing a package for the current user or for the entire machine. The matching parameter is `--scope`, and uses the same values (`user` or `machine`). - -```json - "installBehavior": { - "preferences": { - "scope": "user" - } - }, -``` - -### Locale - -The `locale` behavior affects the choice of installer based on installer locale. The matching parameter is `--locale`, and uses bcp47 language tag. - -```json - "installBehavior": { - "preferences": { - "locale": [ "en-US", "fr-FR" ] - } - }, -``` -### Architectures - -The `architectures` behavior affects what architectures will be selected when installing a package. The matching parameter is `--architecture`. Note that only architectures compatible with your system can be selected. - -```json - "installBehavior": { - "preferences": { - "architectures": ["x64", "arm64"] - } - }, -``` - -### Installer Types - -The `installerTypes` behavior affects what installer types will be selected when installing a package. The matching parameter is `--installer-type`. - -```json - "installBehavior": { - "preferences": { - "installerTypes": ["msi", "msix"] - } - }, -``` - -### Default install root - -The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. - -```json - "installBehavior": { - "defaultInstallRoot": "C:/installRoot" - }, -``` - -## Uninstall Behavior - -The `uninstallBehavior` settings affect the default behavior of uninstalling (where applicable) packages. - -### Purge Portable Package - -The `purgePortablePackage` behavior affects the default behavior for uninstalling a portable package. If set to `true`, uninstall will remove all files and directories relevant to the `portable` package. This setting only applies to packages with the `portable` installer type. Defaults to `false` if value is not set or is invalid. - -```json - "uninstallBehavior": { - "purgePortablePackage": true - }, -``` - -## Telemetry - -The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. - -See [details on telemetry](../README.md#datatelemetry), and our [primary privacy statement](../PRIVACY.md). - -### disable - -```json - "telemetry": { - "disable": true - }, -``` - -If set to true, the `telemetry.disable` setting will prevent any event from being written by the program. - -## Logging - -The `logging` settings control the level of detail in log files. - -### level - - `--verbose-logs` will override this setting and always creates a verbose log. -Defaults to `info` if value is not set or is invalid. - -```json - "logging": { - "level": "verbose" | "info" | "warning" | "error" | "critical" - }, -``` - -### channels - -The valid values in this array are defined in the function `GetChannelFromName` in the [logging code](../src/AppInstallerSharedLib/AppInstallerLogging.cpp). These align with the ***channel identifier*** found in the log files. For example, ***`CORE`*** in: -``` -2023-12-06 19:17:07.988 [CORE] WinGet, version [1.7.0-preview], activity [{24A91EA8-46BE-47A1-B65C-CEBCE90B8675}] -``` - -In addition, there are special values that cover multiple channels. `default` is the default set of channels, while `all` is all of the channels. Invalid values are ignored. - -```json - "logging": { - "channels": ["default"] - }, -``` - -## Network - -The `network` settings influence how winget uses the network to retrieve packages and metadata. - -### Downloader - -The `downloader` setting controls which code is used when downloading packages. The default is `default`, which may be any of the options based on our determination. -`wininet` uses the [WinINet](https://docs.microsoft.com/windows/win32/wininet/about-wininet) APIs, while `do` uses the -[Delivery Optimization](https://support.microsoft.com/windows/delivery-optimization-in-windows-10-0656e53c-15f2-90de-a87a-a2172c94cf6d) service. - -The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. - -```json - "network": { - "downloader": "do", - "doProgressTimeoutInSeconds": 60 - } -``` - -## Interactivity - -The `interactivity` settings control whether winget may show interactive prompts during execution. Note that this refers only to prompts shown by winget itself and not to those shown by package installers. - -### disable - -```json - "interactivity": { - "disable": true - }, -``` - -If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. - -## Experimental Features - -To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. - -The `experimentalFeatures` settings involve the configuration of these "experimental" features. Individual features can be enabled under this node. The example below shows sample experimental features. - -```json - "experimentalFeatures": { - "experimentalCmd": true, - "experimentalArg": false - }, -``` - -### directMSI - -This feature enables the Windows Package Manager to directly install MSI packages with the MSI APIs rather than through msiexec. -Note that when silent installation is used this is already in affect, as MSI packages that require elevation will fail in that scenario without it. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "directMSI": true - }, -``` - -### resume - -This feature enables support for some commands to resume. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "resume": true - }, -``` - -### configuration03 - -This feature enables the configuration schema 0.3. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configuration03": true - }, -``` - -### configureSelfElevate - -This feature enables configure commands to request elevation as needed. -Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context. - -```json - "experimentalFeatures": { - "configureSelfElevate": true - }, -``` - -### configureExport - -This feature enables exporting a configuration file. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configureExport": true - }, -``` +```json + "installBehavior": { + "archiveExtractionMethod": "tar" | "shellApi" + }, +``` + +### Preferences and Requirements + +Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. + +Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. + +### Scope + +The `scope` behavior affects the choice between installing a package for the current user or for the entire machine. The matching parameter is `--scope`, and uses the same values (`user` or `machine`). + +```json + "installBehavior": { + "preferences": { + "scope": "user" + } + }, +``` + +### Locale + +The `locale` behavior affects the choice of installer based on installer locale. The matching parameter is `--locale`, and uses bcp47 language tag. + +```json + "installBehavior": { + "preferences": { + "locale": [ "en-US", "fr-FR" ] + } + }, +``` +### Architectures + +The `architectures` behavior affects what architectures will be selected when installing a package. The matching parameter is `--architecture`. Note that only architectures compatible with your system can be selected. + +```json + "installBehavior": { + "preferences": { + "architectures": ["x64", "arm64"] + } + }, +``` + +### Installer Types + +The `installerTypes` behavior affects what installer types will be selected when installing a package. The matching parameter is `--installer-type`. + +```json + "installBehavior": { + "preferences": { + "installerTypes": ["msi", "msix"] + } + }, +``` + +### Default install root + +The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. + +```json + "installBehavior": { + "defaultInstallRoot": "C:/installRoot" + }, +``` + +## Uninstall Behavior + +The `uninstallBehavior` settings affect the default behavior of uninstalling (where applicable) packages. + +### Purge Portable Package + +The `purgePortablePackage` behavior affects the default behavior for uninstalling a portable package. If set to `true`, uninstall will remove all files and directories relevant to the `portable` package. This setting only applies to packages with the `portable` installer type. Defaults to `false` if value is not set or is invalid. + +```json + "uninstallBehavior": { + "purgePortablePackage": true + }, +``` + +## Telemetry + +The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. + +See [details on telemetry](../README.md#datatelemetry), and our [primary privacy statement](../PRIVACY.md). + +### disable + +```json + "telemetry": { + "disable": true + }, +``` + +If set to true, the `telemetry.disable` setting will prevent any event from being written by the program. + +## Logging + +The `logging` settings control the level of detail in log files. + +### level + + `--verbose-logs` will override this setting and always creates a verbose log. +Defaults to `info` if value is not set or is invalid. + +```json + "logging": { + "level": "verbose" | "info" | "warning" | "error" | "critical" + }, +``` + +### channels + +The valid values in this array are defined in the function `GetChannelFromName` in the [logging code](../src/AppInstallerSharedLib/AppInstallerLogging.cpp). These align with the ***channel identifier*** found in the log files. For example, ***`CORE`*** in: +``` +2023-12-06 19:17:07.988 [CORE] WinGet, version [1.7.0-preview], activity [{24A91EA8-46BE-47A1-B65C-CEBCE90B8675}] +``` + +In addition, there are special values that cover multiple channels. `default` is the default set of channels, while `all` is all of the channels. Invalid values are ignored. + +```json + "logging": { + "channels": ["default"] + }, +``` + +## Network + +The `network` settings influence how winget uses the network to retrieve packages and metadata. + +### Downloader + +The `downloader` setting controls which code is used when downloading packages. The default is `default`, which may be any of the options based on our determination. +`wininet` uses the [WinINet](https://docs.microsoft.com/windows/win32/wininet/about-wininet) APIs, while `do` uses the +[Delivery Optimization](https://support.microsoft.com/windows/delivery-optimization-in-windows-10-0656e53c-15f2-90de-a87a-a2172c94cf6d) service. + +The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. + +```json + "network": { + "downloader": "do", + "doProgressTimeoutInSeconds": 60 + } +``` + +## Interactivity + +The `interactivity` settings control whether winget may show interactive prompts during execution. Note that this refers only to prompts shown by winget itself and not to those shown by package installers. + +### disable + +```json + "interactivity": { + "disable": true + }, +``` + +If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. + +## Experimental Features + +To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. + +The `experimentalFeatures` settings involve the configuration of these "experimental" features. Individual features can be enabled under this node. The example below shows sample experimental features. + +```json + "experimentalFeatures": { + "experimentalCmd": true, + "experimentalArg": false + }, +``` + +### directMSI + +This feature enables the Windows Package Manager to directly install MSI packages with the MSI APIs rather than through msiexec. +Note that when silent installation is used this is already in affect, as MSI packages that require elevation will fail in that scenario without it. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "directMSI": true + }, +``` + +### resume + +This feature enables support for some commands to resume. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "resume": true + }, +``` + +### configuration03 + +This feature enables the configuration schema 0.3. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configuration03": true + }, +``` + +### configureSelfElevate + +This feature enables configure commands to request elevation as needed. +Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context. + +```json + "experimentalFeatures": { + "configureSelfElevate": true + }, +``` + +### configureExport + +This feature enables exporting a configuration file. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configureExport": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 86b8c8842e..ba49e2a21a 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -33,6 +33,11 @@ "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/Command.cpp b/src/AppInstallerCLICore/Command.cpp index b238e32368..54223d9398 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,32 @@ namespace AppInstaller::CLI void Command::OutputIntroHeader(Execution::Reporter& reporter) const { + auto infoOut = reporter.Info(); + VirtualTerminal::ConstructedSequence indent; + + if (VirtualTerminal::SixelsEnabled()) + { + std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); + + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + imagePath /= "Images\\AppList.targetsize-40.png"; + + VirtualTerminal::SixelImage 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); + } + 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 31fd3df970..c5578700a2 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -5,6 +5,7 @@ #if _DEBUG #include "DebugCommand.h" #include +#include "AppInstallerDownloader.h" #include "Sixel.h" using namespace AppInstaller::CLI::Execution; @@ -181,7 +182,24 @@ namespace AppInstaller::CLI void ShowSixelCommand::ExecuteInternal(Execution::Context& context) const { using namespace VirtualTerminal; - SixelImage sixelImage{ Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::Manifest)) }; + std::unique_ptr sixelImagePtr; + + std::string imageUrl{ context.Args.GetArg(Args::Type::Manifest) }; + + 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)); + } + + SixelImage& sixelImage = *sixelImagePtr.get(); if (context.Args.Contains(Args::Type::AcceptPackageAgreements)) { diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index c3723b93df..7e4f110172 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -2,6 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "Sixel.h" +#include +#include #include #include @@ -335,6 +337,52 @@ namespace AppInstaller::CLI::VirtualTerminal m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } + SixelImage::SixelImage(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) + { + InitializeFactory(); + + 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 SixelImage::AspectRatio(SixelAspectRatio aspectRatio) { m_renderControls.AspectRatio = aspectRatio; @@ -406,4 +454,11 @@ namespace AppInstaller::CLI::VirtualTerminal CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_factory))); } + + bool SixelsEnabled() + { + // TODO: Detect support for sixels in current terminal + // You can send a DA1 request("\x1b[c") and you'll get "\x1b[?61;1;...;4;...;41c" back. The "61" is the "conformance level" (61-65 = VT100-500, in that order), but you should ignore that because modern terminals lie about their level. The "4" tells you that the terminal supports sixels and I'd recommend testing for that. + return Settings::User().Get(); + } } diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 2aac3e9b7b..563d9da7e4 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -3,6 +3,7 @@ #pragma once #include "ChannelStreams.h" #include "VTSupport.h" +#include #include #include #include @@ -34,6 +35,7 @@ namespace AppInstaller::CLI::VirtualTerminal static constexpr UINT CellWidthInPixels = 10; SixelImage(const std::filesystem::path& imageFilePath); + SixelImage(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); void AspectRatio(SixelAspectRatio aspectRatio); void Transparency(bool transparencyEnabled); @@ -66,7 +68,7 @@ namespace AppInstaller::CLI::VirtualTerminal struct RenderControls { SixelAspectRatio AspectRatio = SixelAspectRatio::OneToOne; - bool TransparencyEnabled = false; + bool TransparencyEnabled = true; bool StretchSourceToFill = false; bool UseRepeatSequence = false; UINT ColorCount = MaximumColorCount; @@ -82,4 +84,7 @@ namespace AppInstaller::CLI::VirtualTerminal RenderControls m_renderControls; }; + + // Determines if sixels are enabled. + bool SixelsEnabled(); } diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index d16fec7184..37fb3a6ac7 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -100,12 +100,37 @@ namespace AppInstaller::CLI::VirtualTerminal { 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() }; + } + + 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() }; + } - 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 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 @@ -148,7 +173,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() }; } } @@ -158,7 +183,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() }; } } @@ -166,7 +191,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() }; } } @@ -232,7 +257,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 a062eb6d7c..460df7f73a 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -61,10 +61,10 @@ 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); @@ -81,10 +81,10 @@ namespace AppInstaller::CLI::VirtualTerminal { 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..5f3c386b29 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 IsIconBetter(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[ToIntegral(alternative.Resolution)] < s_iconResolutionOrder[ToIntegral(current.Resolution)]; + } + + void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try + { + if (!VirtualTerminal::SixelsEnabled()) + { + return; + } + + auto icons = manifest.CurrentLocalization.Get(); + const Manifest::Icon* bestFitIcon = nullptr; + + for (const auto& icon : icons) + { + if (!bestFitIcon || IsIconBetter(*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::Icons, Utility::SHA256::ConvertToString(bestFitIcon->Sha256), { splitUri.first } }; + auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); + + VirtualTerminal::SixelImage 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/AppInstallerCommonCore/FileCache.cpp b/src/AppInstallerCommonCore/FileCache.cpp index 1594445d44..1c43f23a04 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::Icons: return "Icons"; #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::Icons: #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..bcb53e5574 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, + // Icons + Icons, #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..ec17883259 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -65,6 +65,7 @@ namespace AppInstaller::Settings // Visual ProgressBarVisualStyle, AnonymizePathForDisplay, + EnableSixelDisplay, // Source AutoUpdateTimeInMinutes, // Experimental @@ -147,6 +148,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..982dd64e20 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -29,6 +29,8 @@ 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; + constexpr std::string_view s_ImageAssetsDirectoryRelativePreview = "Images"sv; + constexpr std::string_view s_ImageAssetsDirectoryRelativeRelease = "Assets\\WinGet"sv; 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 +313,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 /= (IsReleaseBuild() ? s_ImageAssetsDirectoryRelativeRelease : s_ImageAssetsDirectoryRelativePreview); + } break; case PathName::CheckpointsLocation: result = GetPathDetailsForPackagedContext(PathName::LocalState, forDisplay); @@ -411,12 +418,22 @@ 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) + { + // Always use preview path for unpackaged + result.Path /= s_ImageAssetsDirectoryRelativePreview; + 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..55636215d8 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -256,6 +256,7 @@ namespace AppInstaller::Settings 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); From 5782f89866af7c83a84d2f7cd06b61c680d3706e Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 17 Sep 2024 17:39:57 -0700 Subject: [PATCH 07/17] Add progress debug command; start on refactoring sixels to enable more complex usage --- .../Commands/DebugCommand.cpp | 74 +++++++++++ .../Commands/DebugCommand.h | 14 +++ src/AppInstallerCLICore/ExecutionReporter.h | 10 +- src/AppInstallerCLICore/Sixel.cpp | 46 +++---- src/AppInstallerCLICore/Sixel.h | 119 ++++++++++++++++-- src/AppInstallerCLICore/pch.h | 1 + .../Public/winget/UserSettings.h | 1 + src/AppInstallerCommonCore/UserSettings.cpp | 15 +-- 8 files changed, 237 insertions(+), 43 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index c5578700a2..5e43a0056e 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -60,6 +60,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -253,6 +254,79 @@ namespace AppInstaller::CLI stream << std::endl; } } + + std::vector ProgressCommand::GetArguments() const + { + return { + Argument{ "sixel", 's', Args::Type::Manifest, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "hide", 'h', Args::Type::AcceptPackageAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "time", 't', Args::Type::AcceptSourceAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "message", 'm', Args::Type::ConfigurationAcceptWarning, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + Argument{ "percent", 'p', Args::Type::AllowReboot, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "post", 0, Args::Type::AllVersions, 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(Args::Type::Manifest)) + { + context.Reporter.SetStyle(Settings::VisualStyle::Sixel); + } + + auto progress = context.Reporter.BeginAsyncProgress(context.Args.Contains(Args::Type::AcceptPackageAgreements)); + + if (context.Args.Contains(Args::Type::ConfigurationAcceptWarning)) + { + progress->Callback().SetProgressMessage(context.Args.GetArg(Args::Type::ConfigurationAcceptWarning)); + } + + bool sendProgress = context.Args.Contains(Args::Type::AllowReboot); + + UINT timeInSeconds = 3600; + if (context.Args.Contains(Args::Type::AcceptSourceAgreements)) + { + timeInSeconds = std::stoul(std::string{ context.Args.GetArg(Args::Type::AcceptSourceAgreements) }); + } + + for (UINT i = 0; i < timeInSeconds; ++i) + { + if (sendProgress) + { + progress->Callback().OnProgress(i, timeInSeconds, ProgressType::Bytes); + } + + if (progress->Callback().IsCancelledBy(CancelReason::Any)) + { + sendProgress = false; + break; + } + + std::this_thread::sleep_for(1s); + } + + if (sendProgress) + { + progress->Callback().OnProgress(timeInSeconds, timeInSeconds, ProgressType::Bytes); + } + + progress.reset(); + + if (context.Args.Contains(Args::Type::AllVersions)) + { + context.Reporter.Info() << context.Args.GetArg(Args::Type::AllVersions) << std::endl; + } + } } #endif diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.h b/src/AppInstallerCLICore/Commands/DebugCommand.h index 36fd05c09f..5e37520b2d 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.h +++ b/src/AppInstallerCLICore/Commands/DebugCommand.h @@ -71,6 +71,20 @@ namespace AppInstaller::CLI 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/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h index 84e7aa6528..33b7b8bba2 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.h +++ b/src/AppInstallerCLICore/ExecutionReporter.h @@ -108,11 +108,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; @@ -179,6 +174,11 @@ namespace AppInstaller::CLI::Execution // 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; diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index 7e4f110172..ae712f2490 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -7,21 +7,21 @@ #include #include -namespace AppInstaller::CLI::VirtualTerminal +namespace AppInstaller::CLI::VirtualTerminal::Sixel { namespace anon { - UINT AspectRatioMultiplier(SixelAspectRatio aspectRatio) + UINT AspectRatioMultiplier(AspectRatio aspectRatio) { switch (aspectRatio) { - case SixelAspectRatio::OneToOne: + case AspectRatio::OneToOne: return 1; - case SixelAspectRatio::TwoToOne: + case AspectRatio::TwoToOne: return 2; - case SixelAspectRatio::ThreeToOne: + case AspectRatio::ThreeToOne: return 3; - case SixelAspectRatio::FiveToOne: + case AspectRatio::FiveToOne: return 5; default: THROW_HR(E_INVALIDARG); @@ -52,12 +52,12 @@ namespace AppInstaller::CLI::VirtualTerminal RenderState( IWICImagingFactory* factory, wil::com_ptr& sourceImage, - const SixelImage::RenderControls& renderControls) : + const Image::RenderControls& renderControls) : m_renderControls(renderControls) { wil::com_ptr currentImage = sourceImage; - if ((renderControls.SizeX && renderControls.SizeY) || renderControls.AspectRatio != SixelAspectRatio::OneToOne) + if ((renderControls.SizeX && renderControls.SizeY) || renderControls.AspectRatio != AspectRatio::OneToOne) { UINT targetX = renderControls.SizeX; UINT targetY = renderControls.SizeY; @@ -167,7 +167,7 @@ namespace AppInstaller::CLI::VirtualTerminal for (size_t i = 0; i < m_palette.size(); ++i) { - // 2 is RGB colorspace, with values from 0 to 100 + // 2 is RGB color space, with values from 0 to 100 stream << '#' << i << ";2;"; WICColor currentColor = m_palette[i]; @@ -187,7 +187,7 @@ namespace AppInstaller::CLI::VirtualTerminal memset(m_sixelBuffer.data(), 0x3F, m_sixelBuffer.size()); // Convert indexed pixel data into per-color sixel lines - UINT rowsToProcess = std::min(SixelImage::PixelsPerSixel, m_lockedImageHeight - m_currentPixelRow); + UINT rowsToProcess = std::min(Image::PixelsPerSixel, m_lockedImageHeight - m_currentPixelRow); size_t imageStride = static_cast(m_lockedImageStride); size_t imageWidth = static_cast(m_lockedImageWidth); const BYTE* currentRowPtr = m_lockedImageBytes + (imageStride * m_currentPixelRow); @@ -307,7 +307,7 @@ namespace AppInstaller::CLI::VirtualTerminal wil::com_ptr m_source; wil::com_ptr m_lockedSource; std::vector m_palette; - SixelImage::RenderControls m_renderControls; + Image::RenderControls m_renderControls; UINT m_lockedImageWidth = 0; UINT m_lockedImageHeight = 0; @@ -324,7 +324,7 @@ namespace AppInstaller::CLI::VirtualTerminal }; } - SixelImage::SixelImage(const std::filesystem::path& imageFilePath) + Image::Image(const std::filesystem::path& imageFilePath) { InitializeFactory(); @@ -337,7 +337,7 @@ namespace AppInstaller::CLI::VirtualTerminal m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - SixelImage::SixelImage(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) + Image::Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) { InitializeFactory(); @@ -383,46 +383,46 @@ namespace AppInstaller::CLI::VirtualTerminal m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - void SixelImage::AspectRatio(SixelAspectRatio aspectRatio) + void Image::AspectRatio(Sixel::AspectRatio aspectRatio) { m_renderControls.AspectRatio = aspectRatio; } - void SixelImage::Transparency(bool transparencyEnabled) + void Image::Transparency(bool transparencyEnabled) { m_renderControls.TransparencyEnabled = transparencyEnabled; } - void SixelImage::ColorCount(UINT colorCount) + void Image::ColorCount(UINT colorCount) { THROW_HR_IF(E_INVALIDARG, colorCount > MaximumColorCount || colorCount < 2); m_renderControls.ColorCount = colorCount; } - void SixelImage::RenderSizeInPixels(UINT x, UINT y) + void Image::RenderSizeInPixels(UINT x, UINT y) { m_renderControls.SizeX = x; m_renderControls.SizeY = y; } - void SixelImage::RenderSizeInCells(UINT x, UINT y) + void Image::RenderSizeInCells(UINT x, UINT y) { // 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 = y * CellHeightInPixels; RenderSizeInPixels(x * CellWidthInPixels, yInPixels - (yInPixels % PixelsPerSixel)); } - void SixelImage::StretchSourceToFill(bool stretchSourceToFill) + void Image::StretchSourceToFill(bool stretchSourceToFill) { m_renderControls.StretchSourceToFill = stretchSourceToFill; } - void SixelImage::UseRepeatSequence(bool useRepeatSequence) + void Image::UseRepeatSequence(bool useRepeatSequence) { m_renderControls.UseRepeatSequence = useRepeatSequence; } - ConstructedSequence SixelImage::Render() + ConstructedSequence Image::Render() { anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; @@ -436,7 +436,7 @@ namespace AppInstaller::CLI::VirtualTerminal return ConstructedSequence{ std::move(result).str() }; } - void SixelImage::RenderTo(Execution::OutputStream& stream) + void Image::RenderTo(Execution::OutputStream& stream) { anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; @@ -446,7 +446,7 @@ namespace AppInstaller::CLI::VirtualTerminal } } - void SixelImage::InitializeFactory() + void Image::InitializeFactory() { THROW_IF_FAILED(CoCreateInstance( CLSID_WICImagingFactory, diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 563d9da7e4..4ab22305f7 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -8,12 +8,12 @@ #include #include -namespace AppInstaller::CLI::VirtualTerminal +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 SixelAspectRatio + enum class AspectRatio { OneToOne = 7, TwoToOne = 0, @@ -21,12 +21,115 @@ namespace AppInstaller::CLI::VirtualTerminal FiveToOne = 2, }; - // Contains an image that can be manipulated and rendered to sixels. - struct SixelImage + // 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; + // 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 + { + // 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); + + // If set to true, the view will % coordinates outside of its dimensions back into its own view. + // If set to false, coordinates outside of the view will be null. + void Tile(bool tile); + + // Translate the view by the given pixel counts. + void Translate(INT x, INT y); + + // Gets the pixel of the view at the given coordinate. + // Returns null if the coordinate is outside of the view. + BYTE* GetPixel(UINT x, UINT y); + + private: + ImageView() = default; + + 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; + }; + + // Contains an image that can be manipulated and rendered to sixels. + struct ImageSource + { + // Create an image source from a file. + ImageSource(const std::filesystem::path& imageFilePath); + + // Create an image source from a stream. + ImageSource(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + + // Resize the image to the given width and height, factoring in the target aspect ratio for rendering. + // If stretchToFill is true, the resulting image will be both the given width and height. + // If false, the resulting image will be at most the given width or height while preserving the aspect ratio. + void Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill = false); + + // Creates a palette from the current image. + Palette CreatePalette(UINT colorCount, bool transparencyEnabled) 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 + { + Compositor() = default; + }; + + // A helpful wrapper around the sixel image primitives that makes rendering a single image easier. + struct Image + { + // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. + static constexpr UINT MaximumColorCount = Palette::MaximumColorCount; + // Yes, its right there in the name but the compiler can't read... static constexpr UINT PixelsPerSixel = 6; @@ -34,10 +137,10 @@ namespace AppInstaller::CLI::VirtualTerminal static constexpr UINT CellHeightInPixels = 20; static constexpr UINT CellWidthInPixels = 10; - SixelImage(const std::filesystem::path& imageFilePath); - SixelImage(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + Image(const std::filesystem::path& imageFilePath); + Image(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); - void AspectRatio(SixelAspectRatio aspectRatio); + void AspectRatio(AspectRatio aspectRatio); void Transparency(bool transparencyEnabled); // If transparency is enabled, one of the colors will be reserved for it. @@ -67,7 +170,7 @@ namespace AppInstaller::CLI::VirtualTerminal // The set of values that defines the rendered output. struct RenderControls { - SixelAspectRatio AspectRatio = SixelAspectRatio::OneToOne; + Sixel::AspectRatio AspectRatio = AspectRatio::OneToOne; bool TransparencyEnabled = true; bool StretchSourceToFill = false; bool UseRepeatSequence = false; diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index e18952c69d..5e65bf018e 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 ) diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index ec17883259..5ac035fa84 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -43,6 +43,7 @@ namespace AppInstaller::Settings Retro, Accent, Rainbow, + Sixel, }; // The download code to use for *installers*. diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 55636215d8..d2d1d23dfe 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -235,23 +235,24 @@ 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; + } return {}; } From 7a642b9207953e94cfa410c673c5a1c7f1acf12f Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 18 Sep 2024 17:49:01 -0700 Subject: [PATCH 08/17] Complete sixel refactor; complete progress visualization refactor --- src/AppInstallerCLICore/Command.cpp | 4 +- .../Commands/DebugCommand.cpp | 22 +- src/AppInstallerCLICore/ExecutionProgress.cpp | 541 +++++++++++------- src/AppInstallerCLICore/ExecutionProgress.h | 87 +-- src/AppInstallerCLICore/ExecutionReporter.cpp | 22 +- src/AppInstallerCLICore/ExecutionReporter.h | 4 +- src/AppInstallerCLICore/Sixel.cpp | 537 ++++++++++++----- src/AppInstallerCLICore/Sixel.h | 123 ++-- .../Workflows/WorkflowBase.cpp | 4 +- src/AppInstallerCLICore/pch.h | 2 + .../Public/winget/UserSettings.h | 1 + src/AppInstallerCommonCore/UserSettings.cpp | 4 + 12 files changed, 846 insertions(+), 505 deletions(-) diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index 54223d9398..fa2049fcd2 100644 --- a/src/AppInstallerCLICore/Command.cpp +++ b/src/AppInstallerCLICore/Command.cpp @@ -46,14 +46,14 @@ namespace AppInstaller::CLI auto infoOut = reporter.Info(); VirtualTerminal::ConstructedSequence indent; - if (VirtualTerminal::SixelsEnabled()) + if (VirtualTerminal::Sixel::SixelsEnabled()) { std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); // This image matches the target pixel size. If changing the target size, choose the most appropriate image. imagePath /= "Images\\AppList.targetsize-40.png"; - VirtualTerminal::SixelImage wingetIcon{ imagePath }; + VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; // Using a height of 2 to match the two lines of header. UINT imageHeightCells = 2; diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index 5e43a0056e..9deb09b1ba 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -183,7 +183,7 @@ namespace AppInstaller::CLI void ShowSixelCommand::ExecuteInternal(Execution::Context& context) const { using namespace VirtualTerminal; - std::unique_ptr sixelImagePtr; + std::unique_ptr sixelImagePtr; std::string imageUrl{ context.Args.GetArg(Args::Type::Manifest) }; @@ -193,30 +193,30 @@ namespace AppInstaller::CLI ProgressCallback emptyCallback; Utility::DownloadToStream(imageUrl, *imageStream, Utility::DownloadType::Manifest, emptyCallback); - sixelImagePtr = std::make_unique(*imageStream, Manifest::IconFileTypeEnum::Unknown); + sixelImagePtr = std::make_unique(*imageStream, Manifest::IconFileTypeEnum::Unknown); } else { - sixelImagePtr = std::make_unique(Utility::ConvertToUTF16(imageUrl)); + sixelImagePtr = std::make_unique(Utility::ConvertToUTF16(imageUrl)); } - SixelImage& sixelImage = *sixelImagePtr.get(); + Sixel::Image& sixelImage = *sixelImagePtr.get(); if (context.Args.Contains(Args::Type::AcceptPackageAgreements)) { switch (context.Args.GetArg(Args::Type::AcceptPackageAgreements)[0]) { case '1': - sixelImage.AspectRatio(SixelAspectRatio::OneToOne); + sixelImage.AspectRatio(Sixel::AspectRatio::OneToOne); break; case '2': - sixelImage.AspectRatio(SixelAspectRatio::TwoToOne); + sixelImage.AspectRatio(Sixel::AspectRatio::TwoToOne); break; case '3': - sixelImage.AspectRatio(SixelAspectRatio::ThreeToOne); + sixelImage.AspectRatio(Sixel::AspectRatio::ThreeToOne); break; case '5': - sixelImage.AspectRatio(SixelAspectRatio::FiveToOne); + sixelImage.AspectRatio(Sixel::AspectRatio::FiveToOne); break; } } @@ -259,6 +259,7 @@ namespace AppInstaller::CLI { return { Argument{ "sixel", 's', Args::Type::Manifest, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, + Argument{ "disabled", 'd', Args::Type::GatedVersion, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, Argument{ "hide", 'h', Args::Type::AcceptPackageAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, Argument{ "time", 't', Args::Type::AcceptSourceAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, Argument{ "message", 'm', Args::Type::ConfigurationAcceptWarning, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, @@ -284,6 +285,11 @@ namespace AppInstaller::CLI context.Reporter.SetStyle(Settings::VisualStyle::Sixel); } + if (context.Args.Contains(Args::Type::GatedVersion)) + { + context.Reporter.SetStyle(Settings::VisualStyle::Disabled); + } + auto progress = context.Reporter.BeginAsyncProgress(context.Args.Contains(Args::Type::AcceptPackageAgreements)); if (context.Args.Contains(Args::Type::ConfigurationAcceptWarning)) diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index 033ef8de16..8e7c3b69e5 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -2,13 +2,14 @@ // Licensed under the MIT License. #include "pch.h" #include "ExecutionProgress.h" +#include "VTSupport.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 { struct BytesFormatData @@ -127,11 +128,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 +198,349 @@ 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 - { - m_out << '\r' << std::string(GetConsoleWidth(), ' ') << '\r'; - } - } - - void ProgressVisualizerBase::Message(std::string_view message) - { - std::atomic_store(&m_message, std::make_shared(message)); } - std::shared_ptr ProgressVisualizerBase::Message() + void StopSpinner() override { - return std::atomic_load(&m_message); + if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + { + m_canceled = true; + m_spinnerJob.get(); + } } - } - void IndefiniteSpinner::ShowSpinner() - { - if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) + void SetMessage(std::string_view message) override { - m_spinnerRunning = true; - m_spinnerJob = std::async(std::launch::async, &IndefiniteSpinner::ShowSpinnerInternal, this); + ProgressVisualizerBase::SetMessage(message); } - } - void IndefiniteSpinner::StopSpinner() - { - if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + std::shared_ptr Message() override { - m_canceled = true; - m_spinnerJob.get(); + return ProgressVisualizerBase::Message(); } - } - - void IndefiniteSpinner::ShowSpinnerInternal() - { - 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); + private: + std::atomic m_canceled = false; + std::atomic m_spinnerRunning = false; + std::future m_spinnerJob; - if (!m_canceled) + void ShowSpinnerInternal() { - if (UseVT()) - { - // Additional VT-based progress reporting, for terminals that support it - m_out << Progress::Construct(Progress::State::Indeterminate); - } + char spinnerChars[] = { '-', '\\', '|', '/' }; - // 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; + // 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); - for (size_t i = 0; !m_canceled; ++i) + if (!m_canceled) { - 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 = this->Message(); - std::string eraser; - if (newMessage) + if (VT_Enabled()) { - size_t newLength = Utility::UTF8ColumnWidth(*newMessage); + // 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; - if (newLength < messageLength) + 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) { - eraser = std::string(messageLength - newLength, ' '); + size_t newLength = Utility::UTF8ColumnWidth(*newMessage); + + if (newLength < messageLength) + { + eraser = std::string(messageLength - newLength, ' '); + } + + message = newMessage; + messageLength = newLength; } - message = newMessage; - messageLength = newLength; + m_out << ' ' << (message ? *message : std::string{}) << eraser << std::flush; + Sleep(250); } - m_out << ' ' << (message ? *message : std::string{}) << eraser << std::flush; - Sleep(250); - } - - ClearLine(); + ClearLine(); - if (UseVT()) - { - m_out << Progress::Construct(Progress::State::None); + if (VT_Enabled()) + { + m_out << Progress::Construct(Progress::State::None); + } } - } - m_canceled = false; - m_spinnerRunning = false; - } - - void ProgressBar::ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) - { - if (current < m_lastCurrent) - { - ClearLine(); + m_canceled = false; + m_spinnerRunning = false; } + }; - // TODO: Progress bar does not currently use message - if (UseVT()) - { - ShowProgressWithVT(current, maximum, type); - } - else - { - ShowProgressNoVT(current, maximum, type); - } - - m_lastCurrent = current; - m_isVisible = true; - } - - void ProgressBar::EndProgress(bool hideProgressWhenDone) + // Displays progress + class CharacterProgressBar : public CharacterProgressVisualizerBase, public IProgressBar { - if (m_isVisible) + public: + CharacterProgressBar(BaseStream& stream, bool enableVT, VisualStyle style) : + CharacterProgressVisualizerBase(stream, enableVT, style) {} + + void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) override { - if (hideProgressWhenDone) + if (current < m_lastCurrent) { ClearLine(); } - else + + // TODO: Progress bar does not currently use message + if (VT_Enabled()) { - m_out << std::endl; + ShowProgressWithVT(current, maximum, type); } - - if (UseVT()) + else { - // 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); + ShowProgressNoVT(current, maximum, type); } - m_isVisible = false; + m_lastCurrent = current; + m_isVisible = true; } - } - - void ProgressBar::ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type) - { - m_out << "\r "; - if (maximum) + void EndProgress(bool hideProgressWhenDone) override { - const char* const blockOn = u8"\x2588"; - const char* const blockOff = u8"\x2592"; - constexpr size_t blockWidth = 30; + if (m_isVisible) + { + if (hideProgressWhenDone) + { + ClearLine(); + } + else + { + m_out << std::endl; + } - double percentage = static_cast(current) / maximum; - size_t blocksOn = static_cast(std::floor(percentage * blockWidth)); + 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); + } - for (size_t i = 0; i < blocksOn; ++i) - { - m_out << blockOn; + m_isVisible = false; } + } - for (size_t i = 0; i < blockWidth - blocksOn; ++i) - { - m_out << blockOff; - } + private: + std::atomic m_isVisible = false; + uint64_t m_lastCurrent = 0; - m_out << " "; + void ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type) + { + m_out << "\r "; - switch (type) + if (maximum) { - 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; + const char* const blockOn = u8"\x2588"; + const char* const blockOff = u8"\x2592"; + constexpr size_t blockWidth = 30; + + double percentage = static_cast(current) / maximum; + size_t blocksOn = static_cast(std::floor(percentage * blockWidth)); + + for (size_t i = 0; i < blocksOn; ++i) + { + m_out << blockOn; + } + + 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) + else { - case AppInstaller::ProgressType::Bytes: - OutputBytes(m_out, current); - break; - case AppInstaller::ProgressType::Percent: - m_out << current << '%'; - break; - default: - m_out << current << " unknowns"; - break; + 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 ProgressBar::ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type) - { - m_out << TextFormat::Default; - - m_out << "\r "; - - if (maximum) + void ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type) { - 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; + m_out << TextFormat::Default; - 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(); + m_out << "\r "; - for (size_t i = 0; i < blockWidth; ++i) + if (maximum) { - ApplyStyle(i, blockWidth, false); + 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; + + 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) - { - m_out << blockOn; + if (i < blocksOn) + { + m_out << blockOn; + } + else if (i == blocksOn) + { + m_out << blocks[partialBlockIndex]; + } + else + { + m_out << blockOff; + } } - else if (i == blocksOn) + + m_out << TextFormat::Default; + + m_out << " "; + + switch (type) { - m_out << blocks[partialBlockIndex]; + 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 + + // Additional VT-based progress reporting, for terminals that support it + m_out << Progress::Construct(Progress::State::Normal, static_cast(percentage * 100)); + } + else + { + switch (type) { - m_out << blockOff; + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + break; + case AppInstaller::ProgressType::Percent: + m_out << current << '%'; + break; + default: + m_out << current << " unknowns"; + break; } } + } + }; - m_out << TextFormat::Default; + std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style) + { + std::unique_ptr result; - m_out << " "; + switch (style) + { + case AppInstaller::Settings::VisualStyle::NoVT: + case AppInstaller::Settings::VisualStyle::Retro: + case AppInstaller::Settings::VisualStyle::Accent: + case AppInstaller::Settings::VisualStyle::Rainbow: + result = std::make_unique(stream, enableVT, style); + break; + case AppInstaller::Settings::VisualStyle::Sixel: + // TODO: The magic + break; + case AppInstaller::Settings::VisualStyle::Disabled: + break; + default: + THROW_HR(E_NOTIMPL); + } - 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; - } + return result; + } - // Additional VT-based progress reporting, for terminals that support it - m_out << Progress::Construct(Progress::State::Normal, static_cast(percentage * 100)); - } - else + std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style) + { + std::unique_ptr result; + + switch (style) { - 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; - } + case AppInstaller::Settings::VisualStyle::NoVT: + case AppInstaller::Settings::VisualStyle::Retro: + case AppInstaller::Settings::VisualStyle::Accent: + case AppInstaller::Settings::VisualStyle::Rainbow: + result = std::make_unique(stream, enableVT, style); + break; + case AppInstaller::Settings::VisualStyle::Sixel: + // TODO: The magic + break; + case AppInstaller::Settings::VisualStyle::Disabled: + break; + default: + THROW_HR(E_NOTIMPL); } + + return result; } } diff --git a/src/AppInstallerCLICore/ExecutionProgress.h b/src/AppInstallerCLICore/ExecutionProgress.h index de49d7e57e..59540451ec 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.h +++ b/src/AppInstallerCLICore/ExecutionProgress.h @@ -1,89 +1,50 @@ // 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 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); }; - // 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); }; } diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp index 8c48a70f3f..089622474f 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.cpp +++ b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -32,8 +32,8 @@ 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_progressBar(IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent)), + m_spinner(IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent)) { SetProgressSink(this); } @@ -103,14 +103,13 @@ 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); + m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style); + m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style); } + if (style == VisualStyle::NoVT) { m_out->SetVTEnabled(false); @@ -244,12 +243,7 @@ namespace AppInstaller::CLI::Execution { if (m_spinner) { - m_spinner->Message(message); - } - - if (m_progressBar) - { - m_progressBar->Message(message); + m_spinner->SetMessage(message); } } diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h index 33b7b8bba2..018a4f3635 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.h +++ b/src/AppInstallerCLICore/ExecutionReporter.h @@ -183,8 +183,8 @@ namespace AppInstaller::CLI::Execution 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 index ae712f2490..b5eb340d57 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -11,6 +11,17 @@ 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) @@ -50,100 +61,16 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel struct RenderState { RenderState( - IWICImagingFactory* factory, - wil::com_ptr& sourceImage, - const Image::RenderControls& renderControls) : + const Palette& palette, + const std::vector& views, + const RenderControls& renderControls) : + m_palette(palette), + m_views(views), m_renderControls(renderControls) { - wil::com_ptr currentImage = sourceImage; - - if ((renderControls.SizeX && renderControls.SizeY) || renderControls.AspectRatio != AspectRatio::OneToOne) - { - UINT targetX = renderControls.SizeX; - UINT targetY = renderControls.SizeY; - - if (!renderControls.StretchSourceToFill) - { - // We need to calculate which of the sizes needs to be reduced - UINT sourceImageX = 0; - UINT sourceImageY = 0; - THROW_IF_FAILED(currentImage->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 /= AspectRatioMultiplier(renderControls.AspectRatio); - - wil::com_ptr scaler; - THROW_IF_FAILED(factory->CreateBitmapScaler(&scaler)); - - THROW_IF_FAILED(scaler->Initialize(currentImage.get(), targetX, targetY, WICBitmapInterpolationModeHighQualityCubic)); - currentImage = CacheToBitmap(factory, scaler.get()); - } - - // Create a color palette - wil::com_ptr palette; - THROW_IF_FAILED(factory->CreatePalette(&palette)); - - THROW_IF_FAILED(palette->InitializeFromBitmap(currentImage.get(), renderControls.ColorCount, renderControls.TransparencyEnabled)); - - // TODO: Determine if the transparent color is always at index 0 - // If not, we should swap it to 0 before conversion to indexed - - // Extract the palette for render use - UINT colorCount = 0; - THROW_IF_FAILED(palette->GetColorCount(&colorCount)); - - m_palette.resize(colorCount); - UINT actualColorCount = 0; - THROW_IF_FAILED(palette->GetColors(colorCount, m_palette.data(), &actualColorCount)); - - // Convert to 8bpp indexed - wil::com_ptr converter; - THROW_IF_FAILED(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(currentImage.get(), GUID_WICPixelFormat8bppIndexed, WICBitmapDitherTypeErrorDiffusion, palette.get(), s_alphaThreshold, WICBitmapPaletteTypeCustom)); - m_source = CacheToBitmap(factory, converter.get()); - - // Lock the image for rendering - UINT sourceX = 0; - UINT sourceY = 0; - THROW_IF_FAILED(currentImage->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(m_source->Lock(&rect, WICBitmapLockRead, &m_lockedSource)); - THROW_IF_FAILED(m_lockedSource->GetSize(&m_lockedImageWidth, &m_lockedImageHeight)); - THROW_IF_FAILED(m_lockedSource->GetStride(&m_lockedImageStride)); - THROW_IF_FAILED(m_lockedSource->GetDataPointer(&m_lockedImageByteCount, &m_lockedImageBytes)); - // Create render buffers - m_enabledColors.resize(m_palette.size()); - m_sixelBuffer.resize(m_palette.size() * m_lockedImageWidth); + m_enabledColors.resize(m_palette.Size()); + m_sixelBuffer.resize(m_palette.Size() * m_renderControls.PixelWidth); } enum class State @@ -165,7 +92,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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) + for (size_t i = 0; i < m_palette.Size(); ++i) { // 2 is RGB color space, with values from 0 to 100 stream << '#' << i << ";2;"; @@ -187,24 +114,41 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel memset(m_sixelBuffer.data(), 0x3F, m_sixelBuffer.size()); // Convert indexed pixel data into per-color sixel lines - UINT rowsToProcess = std::min(Image::PixelsPerSixel, m_lockedImageHeight - m_currentPixelRow); - size_t imageStride = static_cast(m_lockedImageStride); - size_t imageWidth = static_cast(m_lockedImageWidth); - const BYTE* currentRowPtr = m_lockedImageBytes + (imageStride * m_currentPixelRow); + 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 (size_t i = 0; i < imageWidth; ++i) + for (UINT i = 0; i < m_renderControls.PixelWidth; ++i) { - BYTE colorIndex = currentRowPtr[i]; - m_enabledColors[colorIndex] = 1; - m_sixelBuffer[(colorIndex * imageWidth) + i] += sixelBit; - } + 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; + } + } + } - currentRowPtr += imageStride; + if (pixelPtr) + { + m_enabledColors[colorIndex] = 1; + m_sixelBuffer[(colorIndex * m_renderControls.PixelWidth) + i] += sixelBit; + } + } } // Output all sixel color lines @@ -212,16 +156,19 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel for (size_t i = 0; i < m_enabledColors.size(); ++i) { - // Don't output color if transparent - WICColor currentColor = m_palette[i]; - BYTE alpha = (currentColor >> 24) & 0xFF; - if (alpha == 0) - { - continue; - } - 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; @@ -234,17 +181,17 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel stream << '#' << i; - const char* colorRow = &m_sixelBuffer[i * imageWidth]; + const char* colorRow = &m_sixelBuffer[i * m_renderControls.PixelWidth]; if (m_renderControls.UseRepeatSequence) { char currentChar = colorRow[0]; UINT repeatCount = 1; - for (size_t j = 1; j <= imageWidth; ++j) + 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 == imageWidth ? 0 : colorRow[j]); + const char nextChar = (j == m_renderControls.PixelWidth ? 0 : colorRow[j]); if (nextChar == currentChar) { @@ -270,7 +217,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel } else { - stream << std::string_view{ colorRow, imageWidth }; + stream << std::string_view{ colorRow, m_renderControls.PixelWidth }; } } } @@ -279,7 +226,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel stream << '-'; m_currentPixelRow += rowsToProcess; - if (m_currentPixelRow >= m_lockedImageHeight) + if (m_currentPixelRow >= m_renderControls.PixelHeight) { m_currentState = State::Final; } @@ -304,16 +251,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel } private: - wil::com_ptr m_source; - wil::com_ptr m_lockedSource; - std::vector m_palette; - Image::RenderControls m_renderControls; - - UINT m_lockedImageWidth = 0; - UINT m_lockedImageHeight = 0; - UINT m_lockedImageStride = 0; - UINT m_lockedImageByteCount = 0; - BYTE* m_lockedImageBytes = nullptr; + const Palette& m_palette; + const std::vector& m_views; + const RenderControls& m_renderControls; State m_currentState = State::Initial; std::vector m_enabledColors; @@ -324,9 +264,154 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel }; } - Image::Image(const std::filesystem::path& imageFilePath) + 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) + { + // Construct a union of the two palettes + std::set_union(first.m_palette.begin(), first.m_palette.end(), second.m_palette.begin(), second.m_palette.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::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::Tile(bool tile) + { + m_tile = tile; + } + + void ImageView::Translate(INT x, INT y) + { + m_translateX = static_cast(-x); + m_translateY = static_cast(-y); + } + + const BYTE* ImageView::GetPixel(UINT x, UINT y) const { - InitializeFactory(); + 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)); @@ -337,9 +422,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - Image::Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) + ImageSource::ImageSource(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding) { - InitializeFactory(); + m_factory = anon::CreateFactory(); wil::com_ptr stream; THROW_IF_FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &stream)); @@ -383,48 +468,106 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - void Image::AspectRatio(Sixel::AspectRatio aspectRatio) + void ImageSource::Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill) { - m_renderControls.AspectRatio = aspectRatio; + 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, WICBitmapInterpolationModeHighQualityCubic)); + m_sourceImage = anon::CacheToBitmap(m_factory.get(), scaler.get()); + } } - void Image::Transparency(bool transparencyEnabled) + void ImageSource::Resize(const RenderControls& controls) { - m_renderControls.TransparencyEnabled = transparencyEnabled; + Resize(controls.PixelWidth, controls.PixelHeight, controls.AspectRatio, controls.StretchSourceToFill); } - void Image::ColorCount(UINT colorCount) + Palette ImageSource::CreatePalette(UINT colorCount, bool transparencyEnabled) const { - THROW_HR_IF(E_INVALIDARG, colorCount > MaximumColorCount || colorCount < 2); - m_renderControls.ColorCount = colorCount; + return { m_factory.get(), m_sourceImage.get(), colorCount, 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()); } - void Image::RenderSizeInPixels(UINT x, UINT y) + ImageView ImageSource::Lock() const { - m_renderControls.SizeX = x; - m_renderControls.SizeY = y; + return ImageView::Lock(m_sourceImage.get()); } - void Image::RenderSizeInCells(UINT x, UINT y) + ImageView ImageSource::Copy() const { - // 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 = y * CellHeightInPixels; - RenderSizeInPixels(x * CellWidthInPixels, yInPixels - (yInPixels % PixelsPerSixel)); + return ImageView::Copy(m_sourceImage.get()); } - void Image::StretchSourceToFill(bool stretchSourceToFill) + void Compositor::Palette(Sixel::Palette palette) { - m_renderControls.StretchSourceToFill = stretchSourceToFill; + m_palette = std::move(palette); } - void Image::UseRepeatSequence(bool useRepeatSequence) + void Compositor::AddView(ImageView&& view) { - m_renderControls.UseRepeatSequence = useRepeatSequence; + m_views.emplace_back(std::move(view)); } - ConstructedSequence Image::Render() + RenderControls& Compositor::Controls() + { + return m_renderControls; + } + + const RenderControls& Compositor::Controls() const + { + return m_renderControls; + } + + ConstructedSequence Compositor::Render() { - anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; + anon::RenderState renderState{ m_palette, m_views, m_renderControls }; std::stringstream result; @@ -436,9 +579,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel return ConstructedSequence{ std::move(result).str() }; } - void Image::RenderTo(Execution::OutputStream& stream) + void Compositor::RenderTo(Execution::OutputStream& stream) { - anon::RenderState renderState{ m_factory.get(), m_sourceImage, m_renderControls }; + anon::RenderState renderState{ m_palette, m_views, m_renderControls }; while (renderState.Advance()) { @@ -446,19 +589,95 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel } } - void Image::InitializeFactory() + 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) { - THROW_IF_FAILED(CoCreateInstance( - CLSID_WICImagingFactory, - NULL, - CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&m_factory))); + m_renderControls.TransparencyEnabled = transparencyEnabled; + return *this; } - bool SixelsEnabled() + 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.ColorCount, m_renderControls.TransparencyEnabled) }; + 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) }; + } + + bool SixelsSupported() { // TODO: Detect support for sixels in current terminal // You can send a DA1 request("\x1b[c") and you'll get "\x1b[?61;1;...;4;...;41c" back. The "61" is the "conformance level" (61-65 = VT100-500, in that order), but you should ignore that because modern terminals lie about their level. The "4" tells you that the terminal supports sixels and I'd recommend testing for that. - return Settings::User().Get(); + return true; + } + + bool SixelsEnabled() + { + return SixelsSupported() && Settings::User().Get(); } } diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 4ab22305f7..55df984201 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -27,6 +27,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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); @@ -36,10 +39,10 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel Palette(const Palette& first, const Palette& second); // Gets the WIC palette object. - IWICPalette* get() const; + IWICPalette* Get() const; // Gets the color count for the palette. - size_t size() const; + size_t Size() const; // Gets the color at the given index in the palette. WICColor& operator[](size_t index); @@ -67,15 +70,24 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel void Tile(bool tile); // Translate the view by the given pixel counts. + // The pixel at [0, 0] of the original will be at [x, y]. void Translate(INT x, INT y); // Gets the pixel of the view at the given coordinate. // Returns null if the coordinate is outside of the view. - BYTE* GetPixel(UINT x, UINT y); + 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; @@ -86,20 +98,46 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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; + + // 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. - ImageSource(const std::filesystem::path& imageFilePath); + explicit ImageSource(const std::filesystem::path& imageFilePath); // Create an image source from a stream. - ImageSource(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + 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); + // 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; @@ -121,45 +159,64 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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); + + // 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 output 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 { - // Limit to 256 both as the defacto maximum supported colors and to enable always using 8bpp indexed pixel format. - static constexpr UINT MaximumColorCount = Palette::MaximumColorCount; - - // Yes, its right there in the name but the compiler can't read... - static constexpr UINT PixelsPerSixel = 6; + // Create an image from a file. + Image(const std::filesystem::path& imageFilePath); - // 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; + // Create an image from a stream. + Image(std::istream& imageStream, Manifest::IconFileTypeEnum imageEncoding); - Image(const std::filesystem::path& imageFilePath); - Image(std::istream& imageBytes, Manifest::IconFileTypeEnum imageEncoding); + // Set the aspect ratio of the result. + Image& AspectRatio(AspectRatio aspectRatio); - void AspectRatio(AspectRatio aspectRatio); - void Transparency(bool transparencyEnabled); + // 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. - void ColorCount(UINT colorCount); + Image& ColorCount(UINT colorCount); // The resulting sixel image will render to this size in terminal cell pixels. - void RenderSizeInPixels(UINT x, UINT y); + 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. - void RenderSizeInCells(UINT x, UINT y); + 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. - void StretchSourceToFill(bool stretchSourceToFill); + Image& StretchSourceToFill(bool stretchSourceToFill); // Compresses the output using repeat sequences. - void UseRepeatSequence(bool useRepeatSequence); + Image& UseRepeatSequence(bool useRepeatSequence); // Render to sixel format for storage / use multiple times. ConstructedSequence Render(); @@ -167,27 +224,17 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Renders to sixel format directly to the output stream. void RenderTo(Execution::OutputStream& stream); - // The set of values that defines the rendered output. - struct RenderControls - { - Sixel::AspectRatio AspectRatio = AspectRatio::OneToOne; - bool TransparencyEnabled = true; - bool StretchSourceToFill = false; - bool UseRepeatSequence = false; - UINT ColorCount = MaximumColorCount; - UINT SizeX = 0; - UINT SizeY = 0; - }; - private: - void InitializeFactory(); - - wil::com_ptr m_factory; - wil::com_ptr m_sourceImage; + // Creates a compositor for the image using the current render controls. + std::pair CreateCompositor(); + ImageSource m_imageSource; RenderControls m_renderControls; }; + // Determines if sixels are supported by the current instance. + bool SixelsSupported(); + // Determines if sixels are enabled. bool SixelsEnabled(); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 5f3c386b29..41e4636d18 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -99,7 +99,7 @@ namespace AppInstaller::CLI::Workflow void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try { - if (!VirtualTerminal::SixelsEnabled()) + if (!VirtualTerminal::Sixel::SixelsEnabled()) { return; } @@ -125,7 +125,7 @@ namespace AppInstaller::CLI::Workflow Caching::FileCache fileCache{ Caching::FileCache::Type::Icons, Utility::SHA256::ConvertToString(bestFitIcon->Sha256), { splitUri.first } }; auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); - VirtualTerminal::SixelImage sixelIcon{ *iconStream, bestFitIcon->FileType }; + VirtualTerminal::Sixel::Image sixelIcon{ *iconStream, bestFitIcon->FileType }; // Using a height of 4 arbitrarily; allow width up to the entire console. UINT imageHeightCells = 4; diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index 5e65bf018e..069bd33ad9 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -17,6 +17,7 @@ #include #pragma warning( pop ) +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 5ac035fa84..5d8615485a 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -44,6 +44,7 @@ namespace AppInstaller::Settings Accent, Rainbow, Sixel, + Disabled, }; // The download code to use for *installers*. diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index d2d1d23dfe..476971d2f0 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -253,6 +253,10 @@ namespace AppInstaller::Settings { return VisualStyle::Sixel; } + else if (value == "disabled") + { + return VisualStyle::Disabled; + } return {}; } From 52a4b806a92b79102ca38eba37704ba231ce65b4 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 19 Sep 2024 14:33:52 -0700 Subject: [PATCH 09/17] Implement sixel progress --- .../Commands/DebugCommand.cpp | 9 +- src/AppInstallerCLICore/ExecutionProgress.cpp | 307 +++++++++++++++++- src/AppInstallerCLICore/Sixel.cpp | 62 +++- src/AppInstallerCLICore/Sixel.h | 39 ++- 4 files changed, 379 insertions(+), 38 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index 9deb09b1ba..51cb6e49c6 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -305,11 +305,12 @@ namespace AppInstaller::CLI timeInSeconds = std::stoul(std::string{ context.Args.GetArg(Args::Type::AcceptSourceAgreements) }); } - for (UINT i = 0; i < timeInSeconds; ++i) + UINT ticks = timeInSeconds * 10; + for (UINT i = 0; i < ticks; ++i) { if (sendProgress) { - progress->Callback().OnProgress(i, timeInSeconds, ProgressType::Bytes); + progress->Callback().OnProgress(i, ticks, ProgressType::Bytes); } if (progress->Callback().IsCancelledBy(CancelReason::Any)) @@ -318,12 +319,12 @@ namespace AppInstaller::CLI break; } - std::this_thread::sleep_for(1s); + std::this_thread::sleep_for(100ms); } if (sendProgress) { - progress->Callback().OnProgress(timeInSeconds, timeInSeconds, ProgressType::Bytes); + progress->Callback().OnProgress(ticks, ticks, ProgressType::Bytes); } progress.reset(); diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index 8e7c3b69e5..1d810b1aa3 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -3,6 +3,8 @@ #include "pch.h" #include "ExecutionProgress.h" #include "VTSupport.h" +#include "AppInstallerRuntime.h" +#include "Sixel.h" using namespace AppInstaller::Settings; using namespace AppInstaller::CLI::VirtualTerminal; @@ -12,6 +14,8 @@ namespace AppInstaller::CLI::Execution { namespace { + static constexpr size_t s_ProgressBarCellWidth = 30; + struct BytesFormatData { uint64_t PowerOfTwo; @@ -299,7 +303,7 @@ namespace AppInstaller::CLI::Execution } }; - // Displays progress + // Displays progress via character output. class CharacterProgressBar : public CharacterProgressVisualizerBase, public IProgressBar { public: @@ -433,7 +437,7 @@ namespace AppInstaller::CLI::Execution }; const char* const blockOn = blocks[8]; const char* const blockOff = blocks[0]; - constexpr size_t blockWidth = 30; + constexpr size_t blockWidth = s_ProgressBarCellWidth; double percentage = static_cast(current) / maximum; size_t blocksOn = static_cast(std::floor(percentage * blockWidth)); @@ -496,22 +500,286 @@ namespace AppInstaller::CLI::Execution } }; + // 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 imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); + + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + imagePath /= "Images\\AppList.targetsize-20.png"; + + Sixel::ImageSource wingetIcon{ imagePath }; + wingetIcon.Resize(renderControls); + Sixel::Palette palette = wingetIcon.CreatePalette(renderControls); + + // TODO: Move to real locations + m_folder = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\folders_only.png)" }; + m_arrow = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\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); + + m_compositor.Palette(std::move(palette)); + m_compositor.AddView(m_arrow.Copy()); + m_compositor.AddView(m_folder.Copy()); + } + + void ShowSpinner() override + { + if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) + { + m_spinnerRunning = true; + m_spinnerJob = std::async(std::launch::async, &SixelIndefiniteSpinner::ShowSpinnerInternal, this); + } + } + + void StopSpinner() override + { + if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + { + m_canceled = true; + m_spinnerJob.get(); + } + } + + void SetMessage(std::string_view message) override + { + ProgressVisualizerBase::SetMessage(message); + } + + std::shared_ptr Message() override + { + return ProgressVisualizerBase::Message(); + } + + 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 ShowSpinnerInternal() + { + // 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(); + + m_out << Progress::Construct(Progress::State::None); + } + + m_canceled = false; + m_spinnerRunning = false; + } + }; + + // Displays progress with a sixel image. + class SixelProgressBar : public ProgressVisualizerBase, public IProgressBar + { + public: + SixelProgressBar(BaseStream& stream, bool enableVT) : + ProgressVisualizerBase(stream, enableVT) + { + static constexpr UINT s_colorsForBelt = 20; + + Sixel::RenderControls imageRenderControls; + imageRenderControls.RenderSizeInCells(2, 1); + + // This image matches the target pixel size. If changing the target size, choose the most appropriate image. + std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); + imagePath /= "Images\\AppList.targetsize-20.png"; + + m_icon = Sixel::ImageSource{ imagePath }; + m_icon.Resize(imageRenderControls); + imageRenderControls.ColorCount = Sixel::Palette::MaximumColorCount - s_colorsForBelt; + Sixel::Palette iconPalette = m_icon.CreatePalette(imageRenderControls); + + // TODO: Move to real location + m_belt = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\conveyor.png)" }; + m_belt.Resize(imageRenderControls); + imageRenderControls.ColorCount = s_colorsForBelt; + imageRenderControls.InterpolationMode = Sixel::InterpolationMode::Linear; + Sixel::Palette beltPalette = m_belt.CreatePalette(imageRenderControls); + + 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); + } + + void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) override + { + if (current < m_lastCurrent) + { + ClearLine(); + } + + m_out << TextFormat::Default; + + m_out << "\r "; + + if (maximum) + { + + double percentage = static_cast(current) / maximum; + + // Translate icon so that its leading edge is the progress line + INT translation = static_cast((percentage * m_compositor.Controls().PixelWidth) - m_compositor[0].Width()); + + 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; + } + } + + m_lastCurrent = current; + m_isVisible = true; + } + + void EndProgress(bool hideProgressWhenDone) override + { + if (m_isVisible) + { + 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; + } + } + + private: + std::atomic m_isVisible = false; + uint64_t m_lastCurrent = 0; + Sixel::ImageSource m_icon; + Sixel::ImageSource m_belt; + Sixel::Compositor m_compositor; + }; + std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style) { std::unique_ptr result; switch (style) { - case AppInstaller::Settings::VisualStyle::NoVT: - case AppInstaller::Settings::VisualStyle::Retro: - case AppInstaller::Settings::VisualStyle::Accent: - case AppInstaller::Settings::VisualStyle::Rainbow: + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: result = std::make_unique(stream, enableVT, style); break; - case AppInstaller::Settings::VisualStyle::Sixel: - // TODO: The magic + case VisualStyle::Sixel: + if (Sixel::SixelsSupported()) + { + result = std::make_unique(stream, enableVT); + } + else + { + result = std::make_unique(stream, enableVT, VisualStyle::Accent); + } break; - case AppInstaller::Settings::VisualStyle::Disabled: + case VisualStyle::Disabled: break; default: THROW_HR(E_NOTIMPL); @@ -526,16 +794,23 @@ namespace AppInstaller::CLI::Execution switch (style) { - case AppInstaller::Settings::VisualStyle::NoVT: - case AppInstaller::Settings::VisualStyle::Retro: - case AppInstaller::Settings::VisualStyle::Accent: - case AppInstaller::Settings::VisualStyle::Rainbow: + case VisualStyle::NoVT: + case VisualStyle::Retro: + case VisualStyle::Accent: + case VisualStyle::Rainbow: result = std::make_unique(stream, enableVT, style); break; - case AppInstaller::Settings::VisualStyle::Sixel: - // TODO: The magic + case VisualStyle::Sixel: + if (Sixel::SixelsSupported()) + { + result = std::make_unique(stream, enableVT); + } + else + { + result = std::make_unique(stream, enableVT, VisualStyle::Accent); + } break; - case AppInstaller::Settings::VisualStyle::Disabled: + case VisualStyle::Disabled: break; default: THROW_HR(E_NOTIMPL); diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index b5eb340d57..648cffd36f 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -281,8 +281,13 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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(first.m_palette.begin(), first.m_palette.end(), second.m_palette.begin(), second.m_palette.end(), std::back_inserter(m_palette)); + 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; @@ -358,15 +363,20 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel return result; } - void ImageView::Tile(bool tile) + void ImageView::Translate(INT x, INT y, bool tile) { m_tile = tile; - } - void ImageView::Translate(INT x, INT y) - { - m_translateX = static_cast(-x); - m_translateY = static_cast(-y); + 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 @@ -468,7 +478,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel m_sourceImage = anon::CacheToBitmap(m_factory.get(), decodedFrame.get()); } - void ImageSource::Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill) + void ImageSource::Resize(UINT pixelWidth, UINT pixelHeight, AspectRatio targetRenderRatio, bool stretchToFill, InterpolationMode interpolationMode) { if ((pixelWidth && pixelHeight) || targetRenderRatio != AspectRatio::OneToOne) { @@ -507,14 +517,14 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel wil::com_ptr scaler; THROW_IF_FAILED(m_factory->CreateBitmapScaler(&scaler)); - THROW_IF_FAILED(scaler->Initialize(m_sourceImage.get(), targetX, targetY, WICBitmapInterpolationModeHighQualityCubic)); + 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); + Resize(controls.PixelWidth, controls.PixelHeight, controls.AspectRatio, controls.StretchSourceToFill, controls.InterpolationMode); } Palette ImageSource::CreatePalette(UINT colorCount, bool transparencyEnabled) const @@ -522,6 +532,11 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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 @@ -555,6 +570,21 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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; @@ -579,6 +609,16 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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 }; @@ -656,7 +696,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel ImageSource localSource{ m_imageSource }; localSource.Resize(m_renderControls); - Palette palette{ localSource.CreatePalette(m_renderControls.ColorCount, m_renderControls.TransparencyEnabled) }; + Palette palette{ localSource.CreatePalette(m_renderControls) }; localSource.ApplyPalette(palette); ImageView view{ localSource.Lock() }; diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 55df984201..f0d49ca8f8 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -21,6 +21,16 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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 { @@ -65,13 +75,11 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Create a view by copying the pixels from the image. static ImageView Copy(IWICBitmapSource* imageSource); - // If set to true, the view will % coordinates outside of its dimensions back into its own view. - // If set to false, coordinates outside of the view will be null. - void Tile(bool tile); - // Translate the view by the given pixel counts. // The pixel at [0, 0] of the original will be at [x, y]. - void Translate(INT x, INT 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. @@ -115,6 +123,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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. @@ -127,13 +136,16 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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); + 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); @@ -141,6 +153,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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); @@ -168,6 +183,13 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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; @@ -175,7 +197,10 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Render to sixel format for storage / use multiple times. ConstructedSequence Render(); - // Renders to sixel format directly to the output stream. + // 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: From 1ee552d084342b567bf57d333d95378d95a9cfc2 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 19 Sep 2024 17:04:46 -0700 Subject: [PATCH 10/17] Move custom progress images into package location --- doc/Settings.md | 690 +++++++++--------- .../JSON/settings/settings.schema.0.2.json | 4 +- src/AppInstallerCLICore/Command.cpp | 25 +- src/AppInstallerCLICore/ExecutionProgress.cpp | 36 +- .../AppInstallerCLIPackage.wapproj | 3 + .../Images/progress-sixel/arrow_only.png | Bin 0 -> 417 bytes .../Images/progress-sixel/conveyor.png | Bin 0 -> 208 bytes .../Images/progress-sixel/folders_only.png | Bin 0 -> 770 bytes 8 files changed, 386 insertions(+), 372 deletions(-) create mode 100644 src/AppInstallerCLIPackage/Images/progress-sixel/arrow_only.png create mode 100644 src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png create mode 100644 src/AppInstallerCLIPackage/Images/progress-sixel/folders_only.png diff --git a/doc/Settings.md b/doc/Settings.md index 309d09710e..31dbb0ae6d 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -1,345 +1,345 @@ -# WinGet CLI Settings - -You can configure WinGet by editing the `settings.json` file. Running `winget settings` will open the file in the default json editor; if no editor is configured, Windows will prompt for you to select an editor, and Notepad is a sensible option if you have no other preference. - -## File Location - -Settings file is located in %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json - -If you are using the non-packaged WinGet version by building it from source code, the file will be located under %LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json - -## Source - -The `source` settings involve configuration to the WinGet source. - -```json - "source": { - "autoUpdateIntervalInMinutes": 3 - }, -``` - -### autoUpdateIntervalInMinutes - -A positive integer represents the update interval in minutes. The check for updates only happens when a source is used. A zero will disable the check for updates to a source. Any other values are invalid. - -- Disable: 0 -- Default: 5 - -To manually update the source use `winget source update` - -## Visual - -The `visual` settings involve visual elements that are displayed by WinGet - -### progressBar - -Color of the progress bar that WinGet displays when not specified by arguments. - -- accent (default) -- retro -- rainbow - -```json - "visual": { - "progressBar": "accent" - }, -``` - -### anonymizeDisplayedPaths - -Replaces some known folder paths with their respective environment variable. Defaults to true. - -```json - "visual": { - "anonymizeDisplayedPaths": true - }, -``` - -### 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. - -### Disable Install Notes -The `disableInstallNotes` behavior affects whether installation notes are shown after a successful install. Defaults to `false` if value is not set or is invalid. - -```json - "installBehavior": { - "disableInstallNotes": true - }, -``` - -### Portable Package User Root -The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. - -> Note: This setting value must be an absolute path. - -```json - "installBehavior": { - "portablePackageUserRoot": "C:/Users/FooBar/Packages" - }, -``` - -### Portable Package Machine Root -The `portablePackageMachineRoot` setting affects the default root directory where packages are installed to under `Machine` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%PROGRAMFILES%/WinGet/Packages/` if value is not set or is invalid. - -> Note: This setting value must be an absolute path. - -```json - "installBehavior": { - "portablePackageMachineRoot": "C:/Program Files/Packages/Portable" - }, -``` - -### Skip Dependencies -The 'skipDependencies' behavior affects whether dependencies are installed for a given package. Defaults to 'false' if value is not set or is invalid. - -```json - "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. - -```json - "installBehavior": { - "archiveExtractionMethod": "tar" | "shellApi" - }, -``` - -### Preferences and Requirements - -Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. - -Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. - -### Scope - -The `scope` behavior affects the choice between installing a package for the current user or for the entire machine. The matching parameter is `--scope`, and uses the same values (`user` or `machine`). - -```json - "installBehavior": { - "preferences": { - "scope": "user" - } - }, -``` - -### Locale - -The `locale` behavior affects the choice of installer based on installer locale. The matching parameter is `--locale`, and uses bcp47 language tag. - -```json - "installBehavior": { - "preferences": { - "locale": [ "en-US", "fr-FR" ] - } - }, -``` -### Architectures - -The `architectures` behavior affects what architectures will be selected when installing a package. The matching parameter is `--architecture`. Note that only architectures compatible with your system can be selected. - -```json - "installBehavior": { - "preferences": { - "architectures": ["x64", "arm64"] - } - }, -``` - -### Installer Types - -The `installerTypes` behavior affects what installer types will be selected when installing a package. The matching parameter is `--installer-type`. - -```json - "installBehavior": { - "preferences": { - "installerTypes": ["msi", "msix"] - } - }, -``` - -### Default install root - -The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. - -```json - "installBehavior": { - "defaultInstallRoot": "C:/installRoot" - }, -``` - -## Uninstall Behavior - -The `uninstallBehavior` settings affect the default behavior of uninstalling (where applicable) packages. - -### Purge Portable Package - -The `purgePortablePackage` behavior affects the default behavior for uninstalling a portable package. If set to `true`, uninstall will remove all files and directories relevant to the `portable` package. This setting only applies to packages with the `portable` installer type. Defaults to `false` if value is not set or is invalid. - -```json - "uninstallBehavior": { - "purgePortablePackage": true - }, -``` - -## Telemetry - -The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. - -See [details on telemetry](../README.md#datatelemetry), and our [primary privacy statement](../PRIVACY.md). - -### disable - -```json - "telemetry": { - "disable": true - }, -``` - -If set to true, the `telemetry.disable` setting will prevent any event from being written by the program. - -## Logging - -The `logging` settings control the level of detail in log files. - -### level - - `--verbose-logs` will override this setting and always creates a verbose log. -Defaults to `info` if value is not set or is invalid. - -```json - "logging": { - "level": "verbose" | "info" | "warning" | "error" | "critical" - }, -``` - -### channels - -The valid values in this array are defined in the function `GetChannelFromName` in the [logging code](../src/AppInstallerSharedLib/AppInstallerLogging.cpp). These align with the ***channel identifier*** found in the log files. For example, ***`CORE`*** in: -``` -2023-12-06 19:17:07.988 [CORE] WinGet, version [1.7.0-preview], activity [{24A91EA8-46BE-47A1-B65C-CEBCE90B8675}] -``` - -In addition, there are special values that cover multiple channels. `default` is the default set of channels, while `all` is all of the channels. Invalid values are ignored. - -```json - "logging": { - "channels": ["default"] - }, -``` - -## Network - -The `network` settings influence how winget uses the network to retrieve packages and metadata. - -### Downloader - -The `downloader` setting controls which code is used when downloading packages. The default is `default`, which may be any of the options based on our determination. -`wininet` uses the [WinINet](https://docs.microsoft.com/windows/win32/wininet/about-wininet) APIs, while `do` uses the -[Delivery Optimization](https://support.microsoft.com/windows/delivery-optimization-in-windows-10-0656e53c-15f2-90de-a87a-a2172c94cf6d) service. - -The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. - -```json - "network": { - "downloader": "do", - "doProgressTimeoutInSeconds": 60 - } -``` - -## Interactivity - -The `interactivity` settings control whether winget may show interactive prompts during execution. Note that this refers only to prompts shown by winget itself and not to those shown by package installers. - -### disable - -```json - "interactivity": { - "disable": true - }, -``` - -If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. - -## Experimental Features - -To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. - -The `experimentalFeatures` settings involve the configuration of these "experimental" features. Individual features can be enabled under this node. The example below shows sample experimental features. - -```json - "experimentalFeatures": { - "experimentalCmd": true, - "experimentalArg": false - }, -``` - -### directMSI - -This feature enables the Windows Package Manager to directly install MSI packages with the MSI APIs rather than through msiexec. -Note that when silent installation is used this is already in affect, as MSI packages that require elevation will fail in that scenario without it. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "directMSI": true - }, -``` - -### resume - -This feature enables support for some commands to resume. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "resume": true - }, -``` - -### configuration03 - -This feature enables the configuration schema 0.3. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configuration03": true - }, -``` - -### configureSelfElevate - -This feature enables configure commands to request elevation as needed. -Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context. - -```json - "experimentalFeatures": { - "configureSelfElevate": true - }, -``` - -### configureExport - -This feature enables exporting a configuration file. -You can enable the feature as shown below. - -```json - "experimentalFeatures": { - "configureExport": true - }, -``` +# WinGet CLI Settings + +You can configure WinGet by editing the `settings.json` file. Running `winget settings` will open the file in the default json editor; if no editor is configured, Windows will prompt for you to select an editor, and Notepad is a sensible option if you have no other preference. + +## File Location + +Settings file is located in %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json + +If you are using the non-packaged WinGet version by building it from source code, the file will be located under %LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json + +## Source + +The `source` settings involve configuration to the WinGet source. + +```json + "source": { + "autoUpdateIntervalInMinutes": 3 + }, +``` + +### autoUpdateIntervalInMinutes + +A positive integer represents the update interval in minutes. The check for updates only happens when a source is used. A zero will disable the check for updates to a source. Any other values are invalid. + +- Disable: 0 +- Default: 5 + +To manually update the source use `winget source update` + +## Visual + +The `visual` settings involve visual elements that are displayed by WinGet + +### progressBar + +Color of the progress bar that WinGet displays when not specified by arguments. + +- accent (default) +- retro +- rainbow + +```json + "visual": { + "progressBar": "accent" + }, +``` + +### anonymizeDisplayedPaths + +Replaces some known folder paths with their respective environment variable. Defaults to true. + +```json + "visual": { + "anonymizeDisplayedPaths": true + }, +``` + +### 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. + +### Disable Install Notes +The `disableInstallNotes` behavior affects whether installation notes are shown after a successful install. Defaults to `false` if value is not set or is invalid. + +```json + "installBehavior": { + "disableInstallNotes": true + }, +``` + +### Portable Package User Root +The `portablePackageUserRoot` setting affects the default root directory where packages are installed to under `User` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%LOCALAPPDATA%/Microsoft/WinGet/Packages/` if value is not set or is invalid. + +> Note: This setting value must be an absolute path. + +```json + "installBehavior": { + "portablePackageUserRoot": "C:/Users/FooBar/Packages" + }, +``` + +### Portable Package Machine Root +The `portablePackageMachineRoot` setting affects the default root directory where packages are installed to under `Machine` scope. This setting only applies to packages with the `portable` installer type. Defaults to `%PROGRAMFILES%/WinGet/Packages/` if value is not set or is invalid. + +> Note: This setting value must be an absolute path. + +```json + "installBehavior": { + "portablePackageMachineRoot": "C:/Program Files/Packages/Portable" + }, +``` + +### Skip Dependencies +The 'skipDependencies' behavior affects whether dependencies are installed for a given package. Defaults to 'false' if value is not set or is invalid. + +```json + "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. + +```json + "installBehavior": { + "archiveExtractionMethod": "tar" | "shellApi" + }, +``` + +### Preferences and Requirements + +Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. + +Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. + +### Scope + +The `scope` behavior affects the choice between installing a package for the current user or for the entire machine. The matching parameter is `--scope`, and uses the same values (`user` or `machine`). + +```json + "installBehavior": { + "preferences": { + "scope": "user" + } + }, +``` + +### Locale + +The `locale` behavior affects the choice of installer based on installer locale. The matching parameter is `--locale`, and uses bcp47 language tag. + +```json + "installBehavior": { + "preferences": { + "locale": [ "en-US", "fr-FR" ] + } + }, +``` +### Architectures + +The `architectures` behavior affects what architectures will be selected when installing a package. The matching parameter is `--architecture`. Note that only architectures compatible with your system can be selected. + +```json + "installBehavior": { + "preferences": { + "architectures": ["x64", "arm64"] + } + }, +``` + +### Installer Types + +The `installerTypes` behavior affects what installer types will be selected when installing a package. The matching parameter is `--installer-type`. + +```json + "installBehavior": { + "preferences": { + "installerTypes": ["msi", "msix"] + } + }, +``` + +### Default install root + +The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. + +```json + "installBehavior": { + "defaultInstallRoot": "C:/installRoot" + }, +``` + +## Uninstall Behavior + +The `uninstallBehavior` settings affect the default behavior of uninstalling (where applicable) packages. + +### Purge Portable Package + +The `purgePortablePackage` behavior affects the default behavior for uninstalling a portable package. If set to `true`, uninstall will remove all files and directories relevant to the `portable` package. This setting only applies to packages with the `portable` installer type. Defaults to `false` if value is not set or is invalid. + +```json + "uninstallBehavior": { + "purgePortablePackage": true + }, +``` + +## Telemetry + +The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. + +See [details on telemetry](../README.md#datatelemetry), and our [primary privacy statement](../PRIVACY.md). + +### disable + +```json + "telemetry": { + "disable": true + }, +``` + +If set to true, the `telemetry.disable` setting will prevent any event from being written by the program. + +## Logging + +The `logging` settings control the level of detail in log files. + +### level + + `--verbose-logs` will override this setting and always creates a verbose log. +Defaults to `info` if value is not set or is invalid. + +```json + "logging": { + "level": "verbose" | "info" | "warning" | "error" | "critical" + }, +``` + +### channels + +The valid values in this array are defined in the function `GetChannelFromName` in the [logging code](../src/AppInstallerSharedLib/AppInstallerLogging.cpp). These align with the ***channel identifier*** found in the log files. For example, ***`CORE`*** in: +``` +2023-12-06 19:17:07.988 [CORE] WinGet, version [1.7.0-preview], activity [{24A91EA8-46BE-47A1-B65C-CEBCE90B8675}] +``` + +In addition, there are special values that cover multiple channels. `default` is the default set of channels, while `all` is all of the channels. Invalid values are ignored. + +```json + "logging": { + "channels": ["default"] + }, +``` + +## Network + +The `network` settings influence how winget uses the network to retrieve packages and metadata. + +### Downloader + +The `downloader` setting controls which code is used when downloading packages. The default is `default`, which may be any of the options based on our determination. +`wininet` uses the [WinINet](https://docs.microsoft.com/windows/win32/wininet/about-wininet) APIs, while `do` uses the +[Delivery Optimization](https://support.microsoft.com/windows/delivery-optimization-in-windows-10-0656e53c-15f2-90de-a87a-a2172c94cf6d) service. + +The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait without progress before fallback. The default number of seconds is 60, minimum is 1 and the maximum is 600. + +```json + "network": { + "downloader": "do", + "doProgressTimeoutInSeconds": 60 + } +``` + +## Interactivity + +The `interactivity` settings control whether winget may show interactive prompts during execution. Note that this refers only to prompts shown by winget itself and not to those shown by package installers. + +### disable + +```json + "interactivity": { + "disable": true + }, +``` + +If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. + +## Experimental Features + +To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. + +The `experimentalFeatures` settings involve the configuration of these "experimental" features. Individual features can be enabled under this node. The example below shows sample experimental features. + +```json + "experimentalFeatures": { + "experimentalCmd": true, + "experimentalArg": false + }, +``` + +### directMSI + +This feature enables the Windows Package Manager to directly install MSI packages with the MSI APIs rather than through msiexec. +Note that when silent installation is used this is already in affect, as MSI packages that require elevation will fail in that scenario without it. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "directMSI": true + }, +``` + +### resume + +This feature enables support for some commands to resume. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "resume": true + }, +``` + +### configuration03 + +This feature enables the configuration schema 0.3. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configuration03": true + }, +``` + +### configureSelfElevate + +This feature enables configure commands to request elevation as needed. +Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context. + +```json + "experimentalFeatures": { + "configureSelfElevate": true + }, +``` + +### configureExport + +This feature enables exporting a configuration file. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configureExport": true + }, +``` diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index ba49e2a21a..7cc2001c1d 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -26,7 +26,9 @@ "enum": [ "accent", "rainbow", - "retro" + "retro", + "sixel", + "disabled" ] }, "anonymizeDisplayedPaths": { diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index fa2049fcd2..ad1e7cbb6f 100644 --- a/src/AppInstallerCLICore/Command.cpp +++ b/src/AppInstallerCLICore/Command.cpp @@ -48,22 +48,25 @@ namespace AppInstaller::CLI if (VirtualTerminal::Sixel::SixelsEnabled()) { - std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); + std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); - // This image matches the target pixel size. If changing the target size, choose the most appropriate image. - imagePath /= "Images\\AppList.targetsize-40.png"; + 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 }; + VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; - // Using a height of 2 to match the two lines of header. - UINT imageHeightCells = 2; - UINT imageWidthCells = 2 * imageHeightCells; + // 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); + wingetIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); + wingetIcon.RenderTo(infoOut); - indent = VirtualTerminal::Cursor::Position::Forward(static_cast(imageWidthCells)); - infoOut << VirtualTerminal::Cursor::Position::Up(static_cast(imageHeightCells) - 1); + indent = VirtualTerminal::Cursor::Position::Forward(static_cast(imageWidthCells)); + infoOut << VirtualTerminal::Cursor::Position::Up(static_cast(imageHeightCells) - 1); + } } auto productName = Runtime::IsReleaseBuild() ? Resource::String::WindowsPackageManager : Resource::String::WindowsPackageManagerPreview; diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index 1d810b1aa3..e7e82febd1 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -510,18 +510,15 @@ namespace AppInstaller::CLI::Execution renderControls.RenderSizeInCells(2, 1); // Create palette from full image - std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); + std::filesystem::path imageAssetsRoot = Runtime::GetPathTo(Runtime::PathName::ImageAssets); // This image matches the target pixel size. If changing the target size, choose the most appropriate image. - imagePath /= "Images\\AppList.targetsize-20.png"; - - Sixel::ImageSource wingetIcon{ imagePath }; + Sixel::ImageSource wingetIcon{ imageAssetsRoot / "AppList.targetsize-20.png" }; wingetIcon.Resize(renderControls); Sixel::Palette palette = wingetIcon.CreatePalette(renderControls); - // TODO: Move to real locations - m_folder = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\folders_only.png)" }; - m_arrow = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\arrow_only.png)" }; + 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); @@ -636,16 +633,15 @@ namespace AppInstaller::CLI::Execution imageRenderControls.RenderSizeInCells(2, 1); // This image matches the target pixel size. If changing the target size, choose the most appropriate image. - std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::SelfPackageRoot); - imagePath /= "Images\\AppList.targetsize-20.png"; + std::filesystem::path imageAssetsRoot = Runtime::GetPathTo(Runtime::PathName::ImageAssets); - m_icon = Sixel::ImageSource{ imagePath }; + 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); // TODO: Move to real location - m_belt = Sixel::ImageSource{ R"(C:\Users\johnmcp\Pictures\conveyor.png)" }; + m_belt = Sixel::ImageSource{ imageAssetsRoot / "progress-sixel/conveyor.png" }; m_belt.Resize(imageRenderControls); imageRenderControls.ColorCount = s_colorsForBelt; imageRenderControls.InterpolationMode = Sixel::InterpolationMode::Linear; @@ -772,9 +768,14 @@ namespace AppInstaller::CLI::Execution case VisualStyle::Sixel: if (Sixel::SixelsSupported()) { - result = std::make_unique(stream, enableVT); + try + { + result = std::make_unique(stream, enableVT); + } + CATCH_LOG(); } - else + + if (!result) { result = std::make_unique(stream, enableVT, VisualStyle::Accent); } @@ -803,9 +804,14 @@ namespace AppInstaller::CLI::Execution case VisualStyle::Sixel: if (Sixel::SixelsSupported()) { - result = std::make_unique(stream, enableVT); + try + { + result = std::make_unique(stream, enableVT); + } + CATCH_LOG(); } - else + + if (!result) { result = std::make_unique(stream, enableVT, VisualStyle::Accent); } 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 0000000000000000000000000000000000000000..2042dfe79ff09e7be77794f9ef043ae4c1690176 GIT binary patch literal 417 zcmV;S0bc%zP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf5dZ)S5dnW>Uy%R+0X9iQK~y+TV;BV^ z05C#noZ!lewEqkY{~7-M{|BW0Gw4Ol!>fl0O5+57|Nds+Q*vOCFbD&(|3XD^1A+#A z|M8na)-;kqMBSC)$B$o7QQUx_fxmzMV)*wD=;eR^82$iVO9&7&@ZZ0`V8cN8KM`KW z8o-BVg!~tg(P8-U`4z+aH_sTX(^dlG<3GcR*b%G`{hAEyhI zTq_yWyjvKUxuxJPWag0qiq|kGI21AbWn_n{LE-V9&9d}LsLeT2a_V+}z)1OdoUvb{OT00RR6%6FHL9_>D)00000 LNkvXXu0mjfEwii1 literal 0 HcmV?d00001 diff --git a/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png b/src/AppInstallerCLIPackage/Images/progress-sixel/conveyor.png new file mode 100644 index 0000000000000000000000000000000000000000..c9afd7244d0259b70b2c864735cd5f7056e9fd33 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G}c0*}aI1_o{c5N2Gfytfo6nCj``7-Hf7w%?KKfB^@~*VvuU;{SOUP2Quu zbWvl_C6CL&C+CQWmA9Nt`^-I|W7gUE3)qX#ZR^uq>fm*9o1uo1uGG4%ht|lPx#1ZP1_K>z@;j|==^1poj532;bRa{vGf5dZ)S5dnW>Uy%R+0+&fdK~y+Tg;cw4 z6hRQ|*}XfT1wQ~1U_w$vZW-|bWQ1fSNJc#V0g)jP2?;Tgg&`p0Gmwx#&XGvqZ13(o zsH&bh9|7`8?V0YbuCD2sb1;1OcWrrVXYq2Y?;do)Ld<4`iF3YL2VAidr~LNo&+*ag z&o&@rcIU?BYauN6vdzPvac-w+ZbLlZh%I3ea<{yF^XjGRLdYEtOJ-wE;vt6A)}16CcdXf z%!TdVPKS8ZFD~wdZ-+S;0N-*6=3 zWcnT&hhYRWy5f@u&n)cf@0yVA00$#L_e>#xeaP z0w#s(P6Et9Ns{s=F?gyZ5eX-e4IrXHm6|(At0k8dN~Or^%A|;RVJOBb1!&@VSL9=G zqISBjF+0G6^hJi_C8(1UkZ}Y)hKZ}h(^|w`${7@6!yW7MIN$dq>sPiYp9Q;Hw{0!W9-zx&S%$N&HU07*qoM6N<$g06&9 AwEzGB literal 0 HcmV?d00001 From 6b15e2791d0b7e868a9531fe3716a0f9f49070be Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 19 Sep 2024 17:44:44 -0700 Subject: [PATCH 11/17] Start on implementing DA request --- src/AppInstallerCLICore/Sixel.cpp | 16 +++++++++++++--- src/AppInstallerCLICore/VTSupport.h | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index 648cffd36f..726f5e0c2c 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -11,6 +11,17 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel { namespace anon { + // Detect support for sixels in current terminal + bool DetectSixelSupport() + { + if (ConsoleModeRestore::Instance().IsVTEnabled()) + { + // You can send a DA1 request("\x1b[c") and you'll get "\x1b[?61;1;...;4;...;41c" back. The "61" is the "conformance level" (61-65 = VT100-500, in that order), but you should ignore that because modern terminals lie about their level. The "4" tells you that the terminal supports sixels and I'd recommend testing for that. + } + + return false; + } + wil::com_ptr CreateFactory() { wil::com_ptr result; @@ -711,9 +722,8 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel bool SixelsSupported() { - // TODO: Detect support for sixels in current terminal - // You can send a DA1 request("\x1b[c") and you'll get "\x1b[?61;1;...;4;...;41c" back. The "61" is the "conformance level" (61-65 = VT100-500, in that order), but you should ignore that because modern terminals lie about their level. The "4" tells you that the terminal supports sixels and I'd recommend testing for that. - return true; + static bool s_SixelsSupported = anon::DetectSixelSupport(); + return s_SixelsSupported; } bool SixelsEnabled() diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index 460df7f73a..9ea3555c58 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -77,6 +77,26 @@ 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 DA (Device Attributes) request. + struct DeviceAttributes + { + // Queries the device attributes on creation. + DeviceAttributes(); + + // The set of extensions. + enum class Extension + { + + }; + + // 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 From 8bd2dc119e78ddd131f232d5dad1bef6bfdeb142 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 19 Sep 2024 18:04:47 -0700 Subject: [PATCH 12/17] DA interface --- src/AppInstallerCLICore/VTSupport.h | 34 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index 9ea3555c58..d3a0aca6fb 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -77,22 +77,44 @@ 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 DA (Device Attributes) request. - struct DeviceAttributes + // Contains the response to a DA1 (Primary Device Attributes) request. + struct PrimaryDeviceAttributes { - // Queries the device attributes on creation. - DeviceAttributes(); + static const PrimaryDeviceAttributes& Instance(); - // The set of extensions. + // 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: + // Queries the device attributes on creation. + PrimaryDeviceAttributes(); + uint32_t m_conformanceLevel = 0; uint64_t m_extensions = 0; }; From 367dc4664b1c87b88f8e54f04a8d9a8affbb5b8d Mon Sep 17 00:00:00 2001 From: John McPherson Date: Fri, 20 Sep 2024 16:29:49 -0700 Subject: [PATCH 13/17] Implement device attributes and sixel support; fixes for that to work --- src/AppInstallerCLICore/ChannelStreams.cpp | 5 + src/AppInstallerCLICore/ChannelStreams.h | 2 + src/AppInstallerCLICore/Command.cpp | 2 +- src/AppInstallerCLICore/ExecutionProgress.cpp | 8 +- src/AppInstallerCLICore/ExecutionProgress.h | 4 +- src/AppInstallerCLICore/ExecutionReporter.cpp | 36 ++++- src/AppInstallerCLICore/ExecutionReporter.h | 9 ++ src/AppInstallerCLICore/Sixel.cpp | 22 --- src/AppInstallerCLICore/Sixel.h | 6 - src/AppInstallerCLICore/VTSupport.cpp | 142 ++++++++++++++---- src/AppInstallerCLICore/VTSupport.h | 45 ++++-- .../Workflows/WorkflowBase.cpp | 2 +- 12 files changed, 199 insertions(+), 84 deletions(-) diff --git a/src/AppInstallerCLICore/ChannelStreams.cpp b/src/AppInstallerCLICore/ChannelStreams.cpp index 5c1a9f4c5e..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), diff --git a/src/AppInstallerCLICore/ChannelStreams.h b/src/AppInstallerCLICore/ChannelStreams.h index cc216eefb2..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) diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index ad1e7cbb6f..cb0f8f6767 100644 --- a/src/AppInstallerCLICore/Command.cpp +++ b/src/AppInstallerCLICore/Command.cpp @@ -46,7 +46,7 @@ namespace AppInstaller::CLI auto infoOut = reporter.Info(); VirtualTerminal::ConstructedSequence indent; - if (VirtualTerminal::Sixel::SixelsEnabled()) + if (reporter.SixelsEnabled()) { std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index e7e82febd1..f7d96f3290 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -753,7 +753,7 @@ namespace AppInstaller::CLI::Execution Sixel::Compositor m_compositor; }; - std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style) + std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, bool sixelSupported) { std::unique_ptr result; @@ -766,7 +766,7 @@ namespace AppInstaller::CLI::Execution result = std::make_unique(stream, enableVT, style); break; case VisualStyle::Sixel: - if (Sixel::SixelsSupported()) + if (sixelSupported) { try { @@ -789,7 +789,7 @@ namespace AppInstaller::CLI::Execution return result; } - std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style) + std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, bool sixelSupported) { std::unique_ptr result; @@ -802,7 +802,7 @@ namespace AppInstaller::CLI::Execution result = std::make_unique(stream, enableVT, style); break; case VisualStyle::Sixel: - if (Sixel::SixelsSupported()) + if (sixelSupported) { try { diff --git a/src/AppInstallerCLICore/ExecutionProgress.h b/src/AppInstallerCLICore/ExecutionProgress.h index 59540451ec..98d2f508ea 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.h +++ b/src/AppInstallerCLICore/ExecutionProgress.h @@ -30,7 +30,7 @@ namespace AppInstaller::CLI::Execution 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); + static std::unique_ptr CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, bool sixelSupported); }; // Displays a progress bar. @@ -45,6 +45,6 @@ namespace AppInstaller::CLI::Execution virtual void EndProgress(bool hideProgressWhenDone) = 0; // Creates a progress bar for the given style. - static std::unique_ptr CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style); + static std::unique_ptr CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, bool sixelSupported); }; } diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp index 089622474f..9b901cd1b1 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(IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent)), - m_spinner(IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), VisualStyle::Accent)) + m_in(inStream) { + bool sixelSupported = 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 @@ -106,8 +120,9 @@ namespace AppInstaller::CLI::Execution if (m_channel == Channel::Output) { - m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style); - m_progressBar = IProgressBar::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style); + bool sixelSupported = 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) @@ -347,4 +362,15 @@ namespace AppInstaller::CLI::Execution WI_ClearAllFlags(m_enabledLevels, reporterLevel); } } + + bool Reporter::SixelsSupported() + { + auto attributes = GetPrimaryDeviceAttributes(); + return (attributes ? attributes->Supports(PrimaryDeviceAttributes::Extension::Sixel) : false); + } + + bool Reporter::SixelsEnabled() + { + return SixelsSupported() && Settings::User().Get(); + } } diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h index 018a4f3635..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); } @@ -169,6 +172,12 @@ 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. diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index 726f5e0c2c..ec0e2c6bf6 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -11,17 +11,6 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel { namespace anon { - // Detect support for sixels in current terminal - bool DetectSixelSupport() - { - if (ConsoleModeRestore::Instance().IsVTEnabled()) - { - // You can send a DA1 request("\x1b[c") and you'll get "\x1b[?61;1;...;4;...;41c" back. The "61" is the "conformance level" (61-65 = VT100-500, in that order), but you should ignore that because modern terminals lie about their level. The "4" tells you that the terminal supports sixels and I'd recommend testing for that. - } - - return false; - } - wil::com_ptr CreateFactory() { wil::com_ptr result; @@ -719,15 +708,4 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel return { std::move(localSource), std::move(compositor) }; } - - bool SixelsSupported() - { - static bool s_SixelsSupported = anon::DetectSixelSupport(); - return s_SixelsSupported; - } - - bool SixelsEnabled() - { - return SixelsSupported() && Settings::User().Get(); - } } diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index f0d49ca8f8..671ca1995f 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -256,10 +256,4 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel ImageSource m_imageSource; RenderControls m_renderControls; }; - - // Determines if sixels are supported by the current instance. - bool SixelsSupported(); - - // Determines if sixels are enabled. - bool SixelsEnabled(); } diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index 37fb3a6ac7..1f4e0c4517 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,64 +17,105 @@ 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) - { - AICLI_LOG(CLI, Info, << "VT not enabled due to null output handle"); - } - else + bool InitializeMode(DWORD handle, DWORD& previousMode, std::initializer_list modeModifierFallbacks, DWORD disabledFlags = 0) { - if (!GetConsoleMode(hOut, &m_previousMode)) + HANDLE hStd = GetStdHandle(handle); + if (hStd == INVALID_HANDLE_VALUE) { - // 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); + LOG_LAST_ERROR(); + } + else if (hStd == NULL) + { + 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(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 | ENABLE_QUICK_EDIT_MODE); + } + void ConstructedSequence::Append(const Sequence& sequence) { if (!sequence.Get().empty()) @@ -96,6 +137,43 @@ 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 diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h index d3a0aca6fb..ed31e7bb56 100644 --- a/src/AppInstallerCLICore/VTSupport.h +++ b/src/AppInstallerCLICore/VTSupport.h @@ -16,10 +16,29 @@ 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; @@ -29,14 +48,20 @@ 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. @@ -80,7 +105,8 @@ namespace AppInstaller::CLI::VirtualTerminal // Contains the response to a DA1 (Primary Device Attributes) request. struct PrimaryDeviceAttributes { - static const PrimaryDeviceAttributes& Instance(); + // Queries the device attributes on creation. + PrimaryDeviceAttributes(std::ostream& outStream, std::istream& inStream); // The extensions that a device may support. enum class Extension @@ -112,9 +138,6 @@ namespace AppInstaller::CLI::VirtualTerminal bool Supports(Extension extension) const; private: - // Queries the device attributes on creation. - PrimaryDeviceAttributes(); - uint32_t m_conformanceLevel = 0; uint64_t m_extensions = 0; }; diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 41e4636d18..b093398858 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -99,7 +99,7 @@ namespace AppInstaller::CLI::Workflow void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try { - if (!VirtualTerminal::Sixel::SixelsEnabled()) + if (!context.Reporter.SixelsEnabled()) { return; } From 5f57957f6c8e24457a7f0179aaab972628bb96f5 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Sep 2024 13:31:36 -0700 Subject: [PATCH 14/17] Move to not check for sixel support unless settings enabled; add a few tests --- src/AppInstallerCLICore/ExecutionProgress.cpp | 8 +-- src/AppInstallerCLICore/ExecutionProgress.h | 5 +- src/AppInstallerCLICore/ExecutionReporter.cpp | 6 +-- src/AppInstallerCLICore/Sixel.cpp | 4 ++ src/AppInstallerCLICore/Sixel.h | 3 ++ .../AppInstallerCLITests.vcxproj | 1 + .../AppInstallerCLITests.vcxproj.filters | 3 ++ src/AppInstallerCLITests/Sixel.cpp | 49 +++++++++++++++++++ src/AppInstallerCLITests/Strings.cpp | 14 ++++++ 9 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 src/AppInstallerCLITests/Sixel.cpp diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index f7d96f3290..51bfe390c8 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -753,7 +753,7 @@ namespace AppInstaller::CLI::Execution Sixel::Compositor m_compositor; }; - std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, bool sixelSupported) + std::unique_ptr IIndefiniteSpinner::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function& sixelSupported) { std::unique_ptr result; @@ -766,7 +766,7 @@ namespace AppInstaller::CLI::Execution result = std::make_unique(stream, enableVT, style); break; case VisualStyle::Sixel: - if (sixelSupported) + if (sixelSupported()) { try { @@ -789,7 +789,7 @@ namespace AppInstaller::CLI::Execution return result; } - std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, bool sixelSupported) + std::unique_ptr IProgressBar::CreateForStyle(BaseStream& stream, bool enableVT, VisualStyle style, const std::function& sixelSupported) { std::unique_ptr result; @@ -802,7 +802,7 @@ namespace AppInstaller::CLI::Execution result = std::make_unique(stream, enableVT, style); break; case VisualStyle::Sixel: - if (sixelSupported) + if (sixelSupported()) { try { diff --git a/src/AppInstallerCLICore/ExecutionProgress.h b/src/AppInstallerCLICore/ExecutionProgress.h index 98d2f508ea..7085ff3154 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.h +++ b/src/AppInstallerCLICore/ExecutionProgress.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -30,7 +31,7 @@ namespace AppInstaller::CLI::Execution 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, bool sixelSupported); + static std::unique_ptr CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, const std::function& sixelSupported); }; // Displays a progress bar. @@ -45,6 +46,6 @@ namespace AppInstaller::CLI::Execution virtual void EndProgress(bool hideProgressWhenDone) = 0; // Creates a progress bar for the given style. - static std::unique_ptr CreateForStyle(BaseStream& stream, bool enableVT, AppInstaller::Settings::VisualStyle style, bool sixelSupported); + 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 9b901cd1b1..fac4eaf133 100644 --- a/src/AppInstallerCLICore/ExecutionReporter.cpp +++ b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -33,7 +33,7 @@ namespace AppInstaller::CLI::Execution m_out(outStream), m_in(inStream) { - bool sixelSupported = SixelsSupported(); + 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); @@ -120,7 +120,7 @@ namespace AppInstaller::CLI::Execution if (m_channel == Channel::Output) { - bool sixelSupported = SixelsSupported(); + 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); } @@ -371,6 +371,6 @@ namespace AppInstaller::CLI::Execution bool Reporter::SixelsEnabled() { - return SixelsSupported() && Settings::User().Get(); + return Settings::User().Get() && SixelsSupported(); } } diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index ec0e2c6bf6..a12f456a57 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -315,6 +315,10 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel 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{}; diff --git a/src/AppInstallerCLICore/Sixel.h b/src/AppInstallerCLICore/Sixel.h index 671ca1995f..737dde3830 100644 --- a/src/AppInstallerCLICore/Sixel.h +++ b/src/AppInstallerCLICore/Sixel.h @@ -68,6 +68,9 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // 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); 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" }); From 422c5a7fd43bcc8d154ea571fe636abe27a53a9c Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Sep 2024 17:20:11 -0700 Subject: [PATCH 15/17] Fix x86 build; PR feedback macros for debug command args --- .../Commands/DebugCommand.cpp | 96 +++++++++++-------- src/AppInstallerCLICore/VTSupport.cpp | 2 +- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index 51cb6e49c6..e55b993afe 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -155,18 +155,28 @@ namespace AppInstaller::CLI } } +#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', Args::Type::Manifest, Resource::String::SourceListUpdatedNever, ArgumentType::Positional }, - Argument{ "aspect-ratio", 'a', Args::Type::AcceptPackageAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "transparent", 't', Args::Type::AcceptSourceAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "color-count", 'c', Args::Type::ConfigurationAcceptWarning, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "width", 'w', Args::Type::AdminSettingEnable, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "height", 'h', Args::Type::AllowReboot, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "stretch", 's', Args::Type::AllVersions, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "repeat", 'r', Args::Type::Name, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "out-file", 'o', Args::Type::BlockingPin, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + 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 }, }; } @@ -185,7 +195,7 @@ namespace AppInstaller::CLI using namespace VirtualTerminal; std::unique_ptr sixelImagePtr; - std::string imageUrl{ context.Args.GetArg(Args::Type::Manifest) }; + std::string imageUrl{ context.Args.GetArg(WINGET_DEBUG_SIXEL_FILE) }; if (Utility::IsUrlRemote(imageUrl)) { @@ -202,9 +212,9 @@ namespace AppInstaller::CLI Sixel::Image& sixelImage = *sixelImagePtr.get(); - if (context.Args.Contains(Args::Type::AcceptPackageAgreements)) + if (context.Args.Contains(WINGET_DEBUG_SIXEL_ASPECT_RATIO)) { - switch (context.Args.GetArg(Args::Type::AcceptPackageAgreements)[0]) + switch (context.Args.GetArg(WINGET_DEBUG_SIXEL_ASPECT_RATIO)[0]) { case '1': sixelImage.AspectRatio(Sixel::AspectRatio::OneToOne); @@ -221,27 +231,27 @@ namespace AppInstaller::CLI } } - sixelImage.Transparency(context.Args.Contains(Args::Type::AcceptSourceAgreements)); + sixelImage.Transparency(context.Args.Contains(WINGET_DEBUG_SIXEL_TRANSPARENT)); - if (context.Args.Contains(Args::Type::ConfigurationAcceptWarning)) + if (context.Args.Contains(WINGET_DEBUG_SIXEL_COLOR_COUNT)) { - sixelImage.ColorCount(std::stoul(std::string{ context.Args.GetArg(Args::Type::ConfigurationAcceptWarning) })); + sixelImage.ColorCount(std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_SIXEL_COLOR_COUNT) })); } - if (context.Args.Contains(Args::Type::AdminSettingEnable) && context.Args.Contains(Args::Type::AllowReboot)) + if (context.Args.Contains(WINGET_DEBUG_SIXEL_WIDTH) && context.Args.Contains(WINGET_DEBUG_SIXEL_HEIGHT)) { sixelImage.RenderSizeInCells( - std::stoul(std::string{ context.Args.GetArg(Args::Type::AdminSettingEnable) }), - std::stoul(std::string{ context.Args.GetArg(Args::Type::AllowReboot) })); + 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(Args::Type::AllVersions)); + sixelImage.StretchSourceToFill(context.Args.Contains(WINGET_DEBUG_SIXEL_STRETCH)); - sixelImage.UseRepeatSequence(context.Args.Contains(Args::Type::Name)); + sixelImage.UseRepeatSequence(context.Args.Contains(WINGET_DEBUG_SIXEL_REPEAT)); - if (context.Args.Contains(Args::Type::BlockingPin)) + if (context.Args.Contains(WINGET_DEBUG_SIXEL_OUT_FILE)) { - std::ofstream stream{ Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::BlockingPin)) }; + std::ofstream stream{ Utility::ConvertToUTF16(context.Args.GetArg(WINGET_DEBUG_SIXEL_OUT_FILE)) }; stream << sixelImage.Render().Get(); } else @@ -255,16 +265,24 @@ namespace AppInstaller::CLI } } +#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', Args::Type::Manifest, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "disabled", 'd', Args::Type::GatedVersion, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "hide", 'h', Args::Type::AcceptPackageAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "time", 't', Args::Type::AcceptSourceAgreements, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "message", 'm', Args::Type::ConfigurationAcceptWarning, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, - Argument{ "percent", 'p', Args::Type::AllowReboot, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, - Argument{ "post", 0, Args::Type::AllVersions, Resource::String::SourceListUpdatedNever, ArgumentType::Standard }, + 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 }, }; } @@ -280,29 +298,29 @@ namespace AppInstaller::CLI void ProgressCommand::ExecuteInternal(Execution::Context& context) const { - if (context.Args.Contains(Args::Type::Manifest)) + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_SIXEL)) { context.Reporter.SetStyle(Settings::VisualStyle::Sixel); } - if (context.Args.Contains(Args::Type::GatedVersion)) + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_DISABLED)) { context.Reporter.SetStyle(Settings::VisualStyle::Disabled); } - auto progress = context.Reporter.BeginAsyncProgress(context.Args.Contains(Args::Type::AcceptPackageAgreements)); + auto progress = context.Reporter.BeginAsyncProgress(context.Args.Contains(WINGET_DEBUG_PROGRESS_HIDE)); - if (context.Args.Contains(Args::Type::ConfigurationAcceptWarning)) + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_MESSAGE)) { - progress->Callback().SetProgressMessage(context.Args.GetArg(Args::Type::ConfigurationAcceptWarning)); + progress->Callback().SetProgressMessage(context.Args.GetArg(WINGET_DEBUG_PROGRESS_MESSAGE)); } - bool sendProgress = context.Args.Contains(Args::Type::AllowReboot); + bool sendProgress = context.Args.Contains(WINGET_DEBUG_PROGRESS_PERCENT); UINT timeInSeconds = 3600; - if (context.Args.Contains(Args::Type::AcceptSourceAgreements)) + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_TIME)) { - timeInSeconds = std::stoul(std::string{ context.Args.GetArg(Args::Type::AcceptSourceAgreements) }); + timeInSeconds = std::stoul(std::string{ context.Args.GetArg(WINGET_DEBUG_PROGRESS_TIME) }); } UINT ticks = timeInSeconds * 10; @@ -329,9 +347,9 @@ namespace AppInstaller::CLI progress.reset(); - if (context.Args.Contains(Args::Type::AllVersions)) + if (context.Args.Contains(WINGET_DEBUG_PROGRESS_POST)) { - context.Reporter.Info() << context.Args.GetArg(Args::Type::AllVersions) << std::endl; + context.Reporter.Info() << context.Args.GetArg(WINGET_DEBUG_PROGRESS_POST) << std::endl; } } } diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index 1f4e0c4517..e9ce2299cb 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -69,7 +69,7 @@ namespace AppInstaller::CLI::VirtualTerminal inStream.readsome(&result[0], result.size()); THROW_HR_IF(E_UNEXPECTED, static_cast(inStream.gcount()) >= result.size()); - result.resize(inStream.gcount()); + result.resize(static_cast(inStream.gcount())); std::string_view resultView = result; size_t overheadLength = 1 + prefix.length() + suffix.length(); From 5f1f96bae4986f4e84fbf2dd55f6c9d4b1bd0edc Mon Sep 17 00:00:00 2001 From: John McPherson Date: Tue, 24 Sep 2024 11:54:16 -0700 Subject: [PATCH 16/17] PR feedback --- src/AppInstallerCLICore/Sixel.cpp | 6 +----- src/AppInstallerCLICore/VTSupport.cpp | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/AppInstallerCLICore/Sixel.cpp b/src/AppInstallerCLICore/Sixel.cpp index a12f456a57..bae189981c 100644 --- a/src/AppInstallerCLICore/Sixel.cpp +++ b/src/AppInstallerCLICore/Sixel.cpp @@ -50,11 +50,7 @@ namespace AppInstaller::CLI::VirtualTerminal::Sixel // Convert [0, 255] => [0, 100] UINT32 ByteToPercent(BYTE input) { - UINT32 result = static_cast(input); - result *= 100; - UINT32 fractional = result % 255; - result /= 255; - return result + (fractional >= 128 ? 1 : 0); + return (static_cast(input) * 100 + 127) / 255; } // Contains the state for a rendering pass. diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp index e9ce2299cb..1908497bae 100644 --- a/src/AppInstallerCLICore/VTSupport.cpp +++ b/src/AppInstallerCLICore/VTSupport.cpp @@ -113,7 +113,7 @@ namespace AppInstaller::CLI::VirtualTerminal 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 | ENABLE_QUICK_EDIT_MODE); + 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) From 1d4d22c974e0ce3220fc00d5c31ef18a15d4b952 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 26 Sep 2024 09:55:57 -0700 Subject: [PATCH 17/17] PR feedback --- src/AppInstallerCLICore/Command.cpp | 30 +++++++++++-------- src/AppInstallerCLICore/ExecutionProgress.cpp | 2 ++ .../Workflows/WorkflowBase.cpp | 8 ++--- src/AppInstallerCommonCore/FileCache.cpp | 4 +-- .../Public/winget/FileCache.h | 4 +-- src/AppInstallerCommonCore/Runtime.cpp | 13 ++++---- 6 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index cb0f8f6767..066aec0467 100644 --- a/src/AppInstallerCLICore/Command.cpp +++ b/src/AppInstallerCLICore/Command.cpp @@ -48,25 +48,29 @@ namespace AppInstaller::CLI if (reporter.SixelsEnabled()) { - std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); - - if (!imagePath.empty()) + try { - // This image matches the target pixel size. If changing the target size, choose the most appropriate image. - imagePath /= "AppList.targetsize-40.png"; + std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); - VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; + 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"; - // Using a height of 2 to match the two lines of header. - UINT imageHeightCells = 2; - UINT imageWidthCells = 2 * imageHeightCells; + VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; - wingetIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); - wingetIcon.RenderTo(infoOut); + // Using a height of 2 to match the two lines of header. + UINT imageHeightCells = 2; + UINT imageWidthCells = 2 * imageHeightCells; - indent = VirtualTerminal::Cursor::Position::Forward(static_cast(imageWidthCells)); - infoOut << VirtualTerminal::Cursor::Position::Up(static_cast(imageHeightCells) - 1); + 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; diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp index 51bfe390c8..562e293007 100644 --- a/src/AppInstallerCLICore/ExecutionProgress.cpp +++ b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -511,6 +511,7 @@ namespace AppInstaller::CLI::Execution // 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" }; @@ -634,6 +635,7 @@ namespace AppInstaller::CLI::Execution // 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()); m_icon = Sixel::ImageSource{ imageAssetsRoot / "AppList.targetsize-20.png" }; m_icon.Resize(imageRenderControls); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index b093398858..d9510b6a86 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -72,7 +72,7 @@ namespace AppInstaller::CLI::Workflow // 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 IsIconBetter(const Manifest::Icon& current, const Manifest::Icon& alternative) + bool IsSecondIconBetter(const Manifest::Icon& current, const Manifest::Icon& alternative) { static constexpr std::array s_iconResolutionOrder { @@ -94,7 +94,7 @@ namespace AppInstaller::CLI::Workflow 7, // Square256 }; - return s_iconResolutionOrder[ToIntegral(alternative.Resolution)] < s_iconResolutionOrder[ToIntegral(current.Resolution)]; + return s_iconResolutionOrder.at(ToIntegral(alternative.Resolution)) < s_iconResolutionOrder.at(ToIntegral(current.Resolution)); } void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try @@ -109,7 +109,7 @@ namespace AppInstaller::CLI::Workflow for (const auto& icon : icons) { - if (!bestFitIcon || IsIconBetter(*bestFitIcon, icon)) + if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) { bestFitIcon = &icon; } @@ -122,7 +122,7 @@ namespace AppInstaller::CLI::Workflow // Use a cache to hold the icons auto splitUri = Utility::SplitFileNameFromURI(bestFitIcon->Url); - Caching::FileCache fileCache{ Caching::FileCache::Type::Icons, Utility::SHA256::ConvertToString(bestFitIcon->Sha256), { splitUri.first } }; + 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 }; diff --git a/src/AppInstallerCommonCore/FileCache.cpp b/src/AppInstallerCommonCore/FileCache.cpp index 1c43f23a04..23a1ef98ea 100644 --- a/src/AppInstallerCommonCore/FileCache.cpp +++ b/src/AppInstallerCommonCore/FileCache.cpp @@ -17,7 +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::Icons: return "Icons"; + case FileCache::Type::Icon: return "Icon"; #ifndef AICLI_DISABLE_TEST_HOOKS case FileCache::Type::Tests: return "Tests"; #endif @@ -125,7 +125,7 @@ namespace AppInstaller::Caching case Type::IndexV1_Manifest: case Type::IndexV2_PackageVersionData: case Type::IndexV2_Manifest: - case Type::Icons: + case Type::Icon: #ifndef AICLI_DISABLE_TEST_HOOKS case Type::Tests: #endif diff --git a/src/AppInstallerCommonCore/Public/winget/FileCache.h b/src/AppInstallerCommonCore/Public/winget/FileCache.h index bcb53e5574..82fd2af153 100644 --- a/src/AppInstallerCommonCore/Public/winget/FileCache.h +++ b/src/AppInstallerCommonCore/Public/winget/FileCache.h @@ -21,8 +21,8 @@ namespace AppInstaller::Caching IndexV2_PackageVersionData, // Manifests for index V2. IndexV2_Manifest, - // Icons - Icons, + // 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/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp index 982dd64e20..92bb493a83 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -29,8 +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; - constexpr std::string_view s_ImageAssetsDirectoryRelativePreview = "Images"sv; - constexpr std::string_view s_ImageAssetsDirectoryRelativeRelease = "Assets\\WinGet"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; @@ -318,7 +322,7 @@ namespace AppInstaller::Runtime result.Create = false; if (path == PathName::ImageAssets) { - result.Path /= (IsReleaseBuild() ? s_ImageAssetsDirectoryRelativeRelease : s_ImageAssetsDirectoryRelativePreview); + result.Path /= s_ImageAssetsDirectoryRelative; } break; case PathName::CheckpointsLocation: @@ -427,8 +431,7 @@ namespace AppInstaller::Runtime } else if (path == PathName::ImageAssets) { - // Always use preview path for unpackaged - result.Path /= s_ImageAssetsDirectoryRelativePreview; + result.Path /= s_ImageAssetsDirectoryRelative; if (!std::filesystem::is_directory(result.Path)) { result.Path.clear();