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