commit 217e274cc55153f5afc3485474544d18387dcfe8 parent 283d540045223bdb3ef8d54eb66324b778a9a4e5 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Mon, 13 Apr 2020 21:35:32 -0700 Improve progress display (#83) Diffstat:
16 files changed, 660 insertions(+), 143 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -183,6 +183,7 @@ <ClInclude Include="Commands\ValidateCommand.h" /> <ClInclude Include="ExecutionArgs.h" /> <ClInclude Include="ExecutionContext.h" /> + <ClInclude Include="ExecutionProgress.h" /> <ClInclude Include="ExecutionReporter.h" /> <ClInclude Include="Invocation.h" /> <ClInclude Include="Localization.h" /> @@ -210,6 +211,7 @@ <ClCompile Include="Commands\ValidateCommand.cpp" /> <ClCompile Include="Core.cpp" /> <ClCompile Include="ExecutionContext.cpp" /> + <ClCompile Include="ExecutionProgress.cpp" /> <ClCompile Include="ExecutionReporter.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -96,6 +96,9 @@ <ClInclude Include="Commands\ValidateCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="ExecutionProgress.h"> + <Filter>Header Files</Filter> + </ClInclude> <ClInclude Include="TableOutput.h"> <Filter>Header Files</Filter> </ClInclude> @@ -161,6 +164,9 @@ <ClCompile Include="Commands\ValidateCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClCompile Include="ExecutionProgress.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -68,6 +68,12 @@ namespace AppInstaller::CLI return Argument{ "type", 't', Args::Type::SourceType, LOCME("Type of the source"), ArgumentType::Positional }; case Args::Type::ValidateManifest: return Argument{ "manifest", None, Args::Type::ValidateManifest, LOCME("The path to the manifest to be validated"), ArgumentType::Positional, true }; + case Args::Type::NoVT: + return Argument{ "no-vt", None, Args::Type::NoVT, LOCME("Disables VirtualTerminal display"), ArgumentType::Flag, Visibility::Hidden }; + case Args::Type::RainbowStyle: + return Argument{ "rainbow", None, Args::Type::RainbowStyle, LOCME("Progress display a rainbow of colors"), ArgumentType::Flag, Visibility::Hidden }; + case Args::Type::PlainStyle: + return Argument{ "plain", None, Args::Type::PlainStyle, LOCME("Progress display as the default color"), ArgumentType::Flag, Visibility::Hidden }; default: THROW_HR(E_UNEXPECTED); } @@ -76,5 +82,8 @@ namespace AppInstaller::CLI void Argument::GetCommon(std::vector<Argument>& args) { args.push_back(ForType(Args::Type::Help)); + args.push_back(ForType(Args::Type::NoVT)); + args.push_back(ForType(Args::Type::RainbowStyle)); + args.push_back(ForType(Args::Type::PlainStyle)); } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -88,6 +88,7 @@ namespace AppInstaller::CLI Logging::Telemetry().LogCommand(command->FullName()); command->ParseArguments(invocation, context.Args); + context.UpdateForArgs(); command->ValidateArguments(context.Args); } // Exceptions specific to parsing the arguments of a command diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -52,6 +52,9 @@ namespace AppInstaller::CLI::Execution // Other ListVersions, // Used in Show command to list all available versions of an app + NoVT, // Disable VirtualTerminal outputs + PlainStyle, // Makes progress display as plain + RainbowStyle, // Makes progress display as a rainbow Help, // Show command usage }; diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -73,4 +73,20 @@ namespace AppInstaller::CLI::Execution SetCtrlHandlerContext(enabled ? this : nullptr); m_disableCtrlHandlerOnExit = enabled; } + + void Context::UpdateForArgs() + { + if (Args.Contains(Args::Type::NoVT)) + { + Reporter.SetStyle(VisualStyle::NoVT); + } + else if (Args.Contains(Args::Type::PlainStyle)) + { + Reporter.SetStyle(VisualStyle::Rainbow); + } + else if (Args.Contains(Args::Type::RainbowStyle)) + { + Reporter.SetStyle(VisualStyle::Rainbow); + } + } } diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -146,6 +146,9 @@ namespace AppInstaller::CLI::Execution // Only one context can be enabled to handle CTRL signals at a time. void EnableCtrlHandler(bool enabled = true); + // Applies changes based on the parsed args. + void UpdateForArgs(); + // Returns a value indicating whether the context is terminated. bool IsTerminated() const { return m_isTerminated; } diff --git a/src/AppInstallerCLICore/ExecutionProgress.cpp b/src/AppInstallerCLICore/ExecutionProgress.cpp @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ExecutionProgress.h" + + +namespace AppInstaller::CLI::Execution +{ + using namespace VirtualTerminal; + using namespace std::string_view_literals; + + namespace + { + struct BytesFormatData + { + uint64_t PowerOfTwo; + std::string_view Name; + }; + + BytesFormatData s_bytesFormatData[] = + { + // Multi-terabyate installers should be fairly rare for the foreseeable future... + { 40, "TB"sv }, + { 30, "GB"sv }, + { 20, "MB"sv }, + { 10, "KB"sv }, + { 0, "B"sv }, + }; + + const BytesFormatData& GetFormatForSize(uint64_t bytes) + { + for (const auto& format : s_bytesFormatData) + { + if (bytes > (1ull << format.PowerOfTwo)) + { + return format; + } + } + + // Just to make the compiler happy, return the last in the list if we get here. + return s_bytesFormatData[ARRAYSIZE(s_bytesFormatData) - 1]; + } + + void OutputBytes(std::ostream& out, uint64_t byteCount) + { + const BytesFormatData& bfd = GetFormatForSize(byteCount); + + uint64_t integralAmount = byteCount >> bfd.PowerOfTwo; + uint64_t remainder = byteCount & ((1ull << bfd.PowerOfTwo) - 1); + size_t remainderDigits = 0; + + if (integralAmount < 10) + { + remainder *= 100; + remainderDigits = 2; + } + else if (integralAmount < 100) + { + remainder *= 10; + remainderDigits = 1; + } + else if (integralAmount < 1000) + { + // Put an extra space to ensure a consistent 4 chars per numeric output + out << ' '; + } + + out << integralAmount; + + if (remainderDigits) + { + remainder = remainder >> bfd.PowerOfTwo; + out << '.' << std::setw(remainderDigits) << std::setfill('0') << remainder; + } + + out << ' ' << bfd.Name; + } + + void SetColor(std::ostream& out, const TextFormat::Color& color, bool enabled) + { + if (enabled) + { + out << TextFormat::Foreground::Extended(color); + } + else + { + constexpr uint8_t divisor = 3; + + auto reduced = color; + reduced.R /= divisor; + reduced.G /= divisor; + reduced.B /= divisor; + + out << TextFormat::Foreground::Extended(reduced); + } + } + + void SetRainbowColor(std::ostream& out, size_t i, size_t max, bool enabled) + { + TextFormat::Color rainbow[] = + { + { 0xff, 0x00, 0x00 }, + { 0xff, 0x77, 0x00 }, + { 0xff, 0xdd, 0x00 }, + { 0x00, 0xff, 0x00 }, + { 0x00, 0x00, 0xff }, + { 0x8a, 0x2b, 0xe2 }, + { 0xc7, 0x7d, 0xf3 }, + }; + + double target = (static_cast<double>(i) / (max - 1)) * (ARRAYSIZE(rainbow) - 1); + size_t lower = static_cast<size_t>(std::floor(target)); + const auto& lowerVal = rainbow[lower]; + TextFormat::Color result; + + if (lower == (ARRAYSIZE(rainbow) - 1)) + { + result = lowerVal; + } + else + { + double upperContribution = target - lower; + +#define AICLI_AVERAGE(v) static_cast<uint8_t>(((lowerVal.v * (1.0 - upperContribution)) + (rainbow[lower + 1].v * upperContribution))) + result = { AICLI_AVERAGE(R), AICLI_AVERAGE(G), AICLI_AVERAGE(B) }; + } + + SetColor(out, result, enabled); + } + } + + namespace details + { + void ProgressVisualizerBase::ApplyStyle(size_t i, size_t max, bool enabled) + { + switch (m_style) + { + case AppInstaller::CLI::Execution::VisualStyle::NoVT: + // No VT means no style set + break; + case AppInstaller::CLI::Execution::VisualStyle::Plain: + if (enabled) + { + m_out << TextFormat::Default; + } + else + { + m_out << TextFormat::Negative; + } + break; + case AppInstaller::CLI::Execution::VisualStyle::Accent: + SetColor(m_out, TextFormat::Color::GetAccentColor(), enabled); + break; + case AppInstaller::CLI::Execution::VisualStyle::Rainbow: + SetRainbowColor(m_out, i, max, enabled); + break; + default: + LOG_HR(E_UNEXPECTED); + } + } + } + + void IndefiniteSpinner::ShowSpinner() + { + if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) + { + m_spinnerRunning = true; + m_spinnerJob = std::async(std::launch::async, (UseVT() ? &IndefiniteSpinner::ShowSpinnerInternalWithVT : &IndefiniteSpinner::ShowSpinnerInternalNoVT), this); + } + } + + void IndefiniteSpinner::StopSpinner() + { + if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) + { + m_canceled = true; + m_spinnerJob.get(); + } + } + + void IndefiniteSpinner::ShowSpinnerInternalNoVT() + { + 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); + + // Indent two spaces for the spinner, but three here so that we can overwrite it in the loop. + m_out << " "; + + for (size_t i = 0; !m_canceled; ++i) { + constexpr size_t repititionCount = 20; + ApplyStyle(i % repititionCount, repititionCount, true); + m_out << '\b' << spinnerChars[i % ARRAYSIZE(spinnerChars)] << std::flush; + Sleep(250); + } + + m_out << "\b \r"; + m_canceled = false; + m_spinnerRunning = false; + } + + void IndefiniteSpinner::ShowSpinnerInternalWithVT() + { + // Nothing special to do at the moment, can use NoVT version. + ShowSpinnerInternalNoVT(); + } + + void ProgressBar::ShowProgress(uint64_t current, uint64_t maximum, ProgressType type) + { + if (current < m_lastCurrent) + { + ClearLine(); + } + + if (UseVT()) + { + ShowProgressWithVT(current, maximum, type); + } + else + { + ShowProgressNoVT(current, maximum, type); + } + + m_lastCurrent = current; + m_isVisible = true; + } + + void ProgressBar::EndProgress(bool hideProgressWhenDone) + { + if (m_isVisible) + { + if (hideProgressWhenDone) + { + ClearLine(); + } + else + { + m_out << std::endl; + } + m_isVisible = false; + } + } + + void ProgressBar::ClearLine() + { + if (UseVT()) + { + m_out << TextModification::EraseLineEntirely << '\r'; + } + else + { + // Best effort when no VT (arbitrary number of spaces that seems to work) + m_out << "\r \r"; + } + } + + void ProgressBar::ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type) + { + m_out << "\r "; + + if (maximum) + { + const char* const blockOn = u8"\x2588"; + const char* const blockOff = u8"\x2592"; + constexpr size_t blockWidth = 30; + + double percentage = static_cast<double>(current) / maximum; + size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); + + for (size_t i = 0; i < blocksOn; ++i) + { + m_out << blockOn; + } + + for (size_t i = 0; i < blockWidth - blocksOn; ++i) + { + m_out << blockOff; + } + + m_out << " "; + + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + m_out << " / "; + OutputBytes(m_out, maximum); + break; + case AppInstaller::ProgressType::Percent: + default: + m_out << static_cast<int>(percentage * 100) << '%'; + break; + } + } + else + { + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + break; + case AppInstaller::ProgressType::Percent: + m_out << current << '%'; + break; + default: + m_out << current << " unknowns"; + break; + } + } + } + + void ProgressBar::ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type) + { + m_out << "\r "; + + if (maximum) + { + const char* const blockOn = u8"\x2588"; + constexpr size_t blockWidth = 30; + + double percentage = static_cast<double>(current) / maximum; + size_t blocksOn = static_cast<size_t>(std::floor(percentage * blockWidth)); + TextFormat::Color accent = TextFormat::Color::GetAccentColor(); + + for (size_t i = 0; i < blockWidth; ++i) + { + ApplyStyle(i, blockWidth, i < blocksOn); + m_out << blockOn; + } + + m_out << TextFormat::Default; + + m_out << " "; + + switch (type) + { + case AppInstaller::ProgressType::Bytes: + OutputBytes(m_out, current); + m_out << " / "; + OutputBytes(m_out, maximum); + break; + case AppInstaller::ProgressType::Percent: + default: + m_out << static_cast<int>(percentage * 100) << '%'; + break; + } + } + 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; + } + } + } +} diff --git a/src/AppInstallerCLICore/ExecutionProgress.h b/src/AppInstallerCLICore/ExecutionProgress.h @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "VTSupport.h" +#include <AppInstallerProgress.h> + +#include <wil/resource.h> + +#include <atomic> +#include <future> +#include <istream> +#include <ostream> +#include <string> +#include <vector> + + +namespace AppInstaller::CLI::Execution +{ + // The visual style of the progress bar. + enum class VisualStyle + { + NoVT, + Plain, + Accent, + Rainbow, + }; + + namespace details + { + // Shared functionality for progress visualizers. + struct ProgressVisualizerBase + { + ProgressVisualizerBase(std::ostream& stream, bool enableVT) : + m_out(stream), m_enableVT(enableVT) {} + + void SetStyle(VisualStyle style) { m_style = style; } + + protected: + std::ostream& m_out; + VisualStyle m_style = VisualStyle::Accent; + + bool UseVT() const { return m_enableVT && m_style != VisualStyle::NoVT; } + + // Applies the selected visual style. + void ApplyStyle(size_t i, size_t max, bool enabled); + + private: + bool m_enableVT = false; + }; + } + + // Displays an indefinite spinner. + struct IndefiniteSpinner : public details::ProgressVisualizerBase + { + IndefiniteSpinner(std::ostream& stream, bool enableVT) : + details::ProgressVisualizerBase(stream, enableVT) {} + + void ShowSpinner(); + + void StopSpinner(); + + private: + std::atomic<bool> m_canceled = false; + std::atomic<bool> m_spinnerRunning = false; + std::future<void> m_spinnerJob; + + void ShowSpinnerInternalNoVT(); + + void ShowSpinnerInternalWithVT(); + }; + + // Displays progress + class ProgressBar : public details::ProgressVisualizerBase + { + public: + ProgressBar(std::ostream& stream, bool enableVT) : + details::ProgressVisualizerBase(stream, enableVT) {} + + void ShowProgress(uint64_t current, uint64_t maximum, ProgressType type); + + void EndProgress(bool hideProgressWhenDone); + + void SetStyle(VisualStyle style) { m_style = style; } + + private: + std::atomic<bool> m_isVisible = false; + uint64_t m_lastCurrent = 0; + + void ClearLine(); + + void ShowProgressNoVT(uint64_t current, uint64_t maximum, ProgressType type); + + void ShowProgressWithVT(uint64_t current, uint64_t maximum, ProgressType type); + }; +} diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -6,82 +6,23 @@ namespace AppInstaller::CLI::Execution { - VirtualTerminal::Sequence HelpCommandEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; - VirtualTerminal::Sequence HelpArgumentEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; + using namespace VirtualTerminal; - namespace details - { - void IndefiniteSpinner::ShowSpinner() - { - if (!m_spinnerJob.valid() && !m_spinnerRunning && !m_canceled) - { - m_spinnerRunning = true; - m_spinnerJob = std::async(std::launch::async, &IndefiniteSpinner::ShowSpinnerInternal, this); - } - } - - void IndefiniteSpinner::StopSpinner() - { - if (!m_canceled && m_spinnerJob.valid() && m_spinnerRunning) - { - m_canceled = true; - m_spinnerJob.get(); - } - } - - void IndefiniteSpinner::ShowSpinnerInternal() - { - char spinnerChars[] = { '-', '\\', '|', '/' }; - - // First wait for a small amount of time to enable a fast task to skip - // showing anything, or a progress task to skip straight to progress. - Sleep(100); - - for (int i = 0; !m_canceled; i++) { - m_out << '\b' << spinnerChars[i] << std::flush; - - if (i == 3) - { - i = -1; - } - - Sleep(250); - } + const Sequence& HelpCommandEmphasis = TextFormat::Foreground::BrightWhite; + const Sequence& HelpArgumentEmphasis = TextFormat::Foreground::BrightWhite; - m_out << '\b'; - m_canceled = false; - m_spinnerRunning = false; - } - - void ProgressBar::ShowProgress(bool running, uint64_t progress) - { - if (running) - { - if (m_isVisible) - { - m_out << "\rProgress: " << progress; - } - else - { - m_out << "Progress: " << progress; - m_isVisible = true; - } - } - else - { - if (m_isVisible) - { - m_out << std::endl; - m_isVisible = false; - } - } - } - } + Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : + m_out(outStream), + m_in(inStream), + m_consoleMode(), + m_progressBar(outStream, m_consoleMode.IsVTEnabled()), + m_spinner(outStream, m_consoleMode.IsVTEnabled()) + {} Reporter::OutputStream::OutputStream(std::ostream& out, bool enableVT) : m_out(out), m_isVTEnabled(enableVT) {} - void Reporter::OutputStream::AddFormat(const VirtualTerminal::Sequence& sequence) + void Reporter::OutputStream::AddFormat(const Sequence& sequence) { m_format.append(sequence.Get()); } @@ -104,7 +45,7 @@ namespace AppInstaller::CLI::Execution return *this; } - Reporter::OutputStream& Reporter::OutputStream::operator<<(const VirtualTerminal::Sequence& sequence) + Reporter::OutputStream& Reporter::OutputStream::operator<<(const Sequence& sequence) { m_out << sequence; // An incoming sequence will be valid for 1 "standard" output after this one. @@ -120,7 +61,7 @@ namespace AppInstaller::CLI::Execution // For now, we assume this means "default". if (m_consoleMode.IsVTEnabled()) { - m_out << VirtualTerminal::TextFormat::Default; + m_out << TextFormat::Default; } } @@ -131,16 +72,16 @@ namespace AppInstaller::CLI::Execution switch (level) { case Level::Verbose: - result.AddFormat(VirtualTerminal::TextFormat::Default); + result.AddFormat(TextFormat::Default); break; case Level::Info: - result.AddFormat(VirtualTerminal::TextFormat::Default); + result.AddFormat(TextFormat::Default); break; case Level::Warning: - result.AddFormat(VirtualTerminal::TextFormat::Foreground::BrightYellow); + result.AddFormat(TextFormat::Foreground::BrightYellow); break; case Level::Error: - result.AddFormat(VirtualTerminal::TextFormat::Foreground::BrightRed); + result.AddFormat(TextFormat::Foreground::BrightRed); break; default: THROW_HR(E_UNEXPECTED); @@ -149,6 +90,16 @@ namespace AppInstaller::CLI::Execution return result; } + void Reporter::SetStyle(VisualStyle style) + { + m_spinner.SetStyle(style); + m_progressBar.SetStyle(style); + if (style == VisualStyle::NoVT) + { + m_consoleMode.DisableVT(); + } + } + bool Reporter::PromptForBoolResponse(const std::string& msg, Level level) { UNREFERENCED_PARAMETER(level); @@ -161,11 +112,6 @@ namespace AppInstaller::CLI::Execution return tolower(response) == 'y'; } - void Reporter::ShowProgress(bool running, uint64_t progress) - { - m_progressBar.ShowProgress(running, progress); - } - void Reporter::ShowIndefiniteProgress(bool running) { if (running) @@ -180,9 +126,8 @@ namespace AppInstaller::CLI::Execution void Reporter::OnProgress(uint64_t current, uint64_t maximum, ProgressType type) { - UNREFERENCED_PARAMETER(type); ShowIndefiniteProgress(false); - ShowProgress(true, (maximum ? static_cast<uint64_t>((static_cast<double>(current) / maximum) * 100) : current)); + m_progressBar.ShowProgress(current, maximum, type); } void Reporter::SetProgressCallback(ProgressCallback* callback) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -1,55 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include "ExecutionProgress.h" #include "VTSupport.h" #include <AppInstallerProgress.h> #include <wil/resource.h> #include <atomic> -#include <future> #include <istream> #include <ostream> #include <string> -#include <vector> namespace AppInstaller::CLI::Execution { - namespace details - { - // Class to print a indefinite spinner. - class IndefiniteSpinner - { - public: - IndefiniteSpinner(std::ostream& stream) : m_out(stream) {} - - void ShowSpinner(); - void StopSpinner(); - - private: - std::atomic<bool> m_canceled = false; - std::atomic<bool> m_spinnerRunning = false; - std::future<void> m_spinnerJob; - std::ostream& m_out; - - void ShowSpinnerInternal(); - }; - - // Todo: Need to implement real progress bar. Only prints progress number now. - class ProgressBar - { - public: - ProgressBar(std::ostream& stream) : m_out(stream) {} - - void ShowProgress(bool running, uint64_t progress); - - private: - std::atomic<bool> m_isVisible = false; - std::ostream& m_out; - }; - } - // Reporter should be the central place to show workflow status to user. // Todo: need to implement actual console output to show progress bar, etc struct Reporter : public IProgressSink @@ -62,8 +27,7 @@ namespace AppInstaller::CLI::Execution Error, }; - Reporter(std::ostream& outStream, std::istream& inStream) : - m_out(outStream), m_in(inStream), m_progressBar(outStream), m_spinner(outStream) {} + Reporter(std::ostream& outStream, std::istream& inStream); ~Reporter(); @@ -113,11 +77,10 @@ namespace AppInstaller::CLI::Execution void EmptyLine() { m_out << std::endl; } - bool PromptForBoolResponse(const std::string& msg, Level level = Level::Info); + // Sets the visual style (mostly for progress currently) + void SetStyle(VisualStyle style); - // Used to show definite progress. - // running: shows progress bar if set to true, dismisses progress bar if set to false - void ShowProgress(bool running, uint64_t progress); + bool PromptForBoolResponse(const std::string& msg, Level level = Level::Info); // Used to show indefinite progress. Currently an indefinite spinner is the form of // showing indefinite progress. @@ -129,17 +92,27 @@ namespace AppInstaller::CLI::Execution // Runs the given callable of type: auto(IProgressCallback&) template <typename F> - auto ExecuteWithProgress(F&& f) + auto ExecuteWithProgress(F&& f, bool hideProgressWhenDone = false) { + if (m_consoleMode.IsVTEnabled()) + { + m_out << VirtualTerminal::Cursor::Visibility::DisableShow; + } + ProgressCallback callback(this); SetProgressCallback(&callback); ShowIndefiniteProgress(true); - auto hideProgress = wil::scope_exit([this]() + auto hideProgress = wil::scope_exit([this, hideProgressWhenDone]() { SetProgressCallback(nullptr); ShowIndefiniteProgress(false); - ShowProgress(false, 0); + m_progressBar.EndProgress(hideProgressWhenDone); + + if (m_consoleMode.IsVTEnabled()) + { + m_out << VirtualTerminal::Cursor::Visibility::EnableShow; + } }); return f(callback); } @@ -154,13 +127,13 @@ namespace AppInstaller::CLI::Execution std::ostream& m_out; std::istream& m_in; VirtualTerminal::ConsoleModeRestore m_consoleMode; - details::IndefiniteSpinner m_spinner; - details::ProgressBar m_progressBar; + IndefiniteSpinner m_spinner; + ProgressBar m_progressBar; wil::srwlock m_progressCallbackLock; std::atomic<ProgressCallback*> m_progressCallback; }; // Indirection to enable change without tracking down every place - extern VirtualTerminal::Sequence HelpCommandEmphasis; - extern VirtualTerminal::Sequence HelpArgumentEmphasis; + extern const VirtualTerminal::Sequence& HelpCommandEmphasis; + extern const VirtualTerminal::Sequence& HelpArgumentEmphasis; } diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp @@ -6,6 +6,18 @@ namespace AppInstaller::CLI::VirtualTerminal { + namespace + { + TextFormat::Color GetAccentColorFromSystem() + { + using namespace winrt::Windows::UI::ViewManagement; + + UISettings settings; + auto color = settings.GetColorValue(UIColorType::Accent); + return { color.R, color.G, color.B }; + } + } + ConsoleModeRestore::ConsoleModeRestore(bool enableVTProcessing) { if (enableVTProcessing) @@ -69,23 +81,65 @@ namespace AppInstaller::CLI::VirtualTerminal // The beginning of an Operating system command #define AICLI_VT_OSC AICLI_VT_ESCAPE "]" + namespace Cursor + { + namespace Position + { +#define AICLI_VT_SIMPLE_CURSORPOSITON(_c_) AICLI_VT_ESCAPE #_c_ + + 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); + } + + namespace Visibility + { + const Sequence EnableBlink = AICLI_VT_CSI "?12h"; + const Sequence DisableBlink = AICLI_VT_CSI "?12l"; + const Sequence EnableShow = AICLI_VT_CSI "?25h"; + const Sequence DisableShow = AICLI_VT_CSI "?25l"; + } + } + namespace TextFormat { // Define a text formatting sequence with an integer id #define AICLI_VT_TEXTFORMAT(_id_) AICLI_VT_CSI #_id_ "m" - Sequence Default = AICLI_VT_TEXTFORMAT(0); + const Sequence Default = AICLI_VT_TEXTFORMAT(0); + const Sequence Negative = AICLI_VT_TEXTFORMAT(7); + + Color Color::GetAccentColor() + { + static Color accent = GetAccentColorFromSystem(); + return accent; + } namespace Foreground { - Sequence BrightRed = AICLI_VT_TEXTFORMAT(91); - Sequence BrightYellow = AICLI_VT_TEXTFORMAT(93); - Sequence BrightWhite = AICLI_VT_TEXTFORMAT(97); + const Sequence BrightRed = AICLI_VT_TEXTFORMAT(91); + const Sequence BrightYellow = AICLI_VT_TEXTFORMAT(93); + const Sequence BrightWhite = AICLI_VT_TEXTFORMAT(97); + + ConstructedSequence Extended(const Color& color) + { + std::ostringstream result; + result << AICLI_VT_CSI "38;2;" << static_cast<uint32_t>(color.R) << ';' << static_cast<uint32_t>(color.G) << ';' << static_cast<uint32_t>(color.B) << 'm'; + return result.str(); + } } namespace Background { } - }; + } + + namespace TextModification + { + const Sequence EraseLineForward = AICLI_VT_CSI "0K"; + const Sequence EraseLineBackward = AICLI_VT_CSI "1K"; + const Sequence EraseLineEntirely = AICLI_VT_CSI "2K"; + } } diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h @@ -22,6 +22,8 @@ namespace AppInstaller::CLI::VirtualTerminal ConsoleModeRestore(ConsoleModeRestore&&) = default; ConsoleModeRestore& operator=(ConsoleModeRestore&&) = default; + void DisableVT() { m_isVTEnabled = false; } + bool IsVTEnabled() const { return m_isVTEnabled; } private: @@ -57,16 +59,50 @@ namespace AppInstaller::CLI::VirtualTerminal // Below are mapped to the sequences described here: // https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + namespace Cursor + { + namespace Position + { + extern const Sequence UpOne; + extern const Sequence DownOne; + extern const Sequence ForwardOne; + extern const Sequence BackwardOne; + } + + namespace Visibility + { + extern const Sequence EnableBlink; + extern const Sequence DisableBlink; + extern const Sequence EnableShow; + extern const Sequence DisableShow; + } + } + namespace TextFormat { // Returns all attributes to the default state prior to modification - extern Sequence Default; + extern const Sequence Default; + + // Swaps foreground and background colors + extern const Sequence Negative; + + // A color, used in constructed sequences. + struct Color + { + uint8_t R; + uint8_t G; + uint8_t B; + + static Color GetAccentColor(); + }; namespace Foreground { - extern Sequence BrightRed; - extern Sequence BrightYellow; - extern Sequence BrightWhite; + extern const Sequence BrightRed; + extern const Sequence BrightYellow; + extern const Sequence BrightWhite; + + ConstructedSequence Extended(const Color& color); } namespace Background @@ -74,6 +110,13 @@ namespace AppInstaller::CLI::VirtualTerminal } } + + namespace TextModification + { + extern const Sequence EraseLineForward; + extern const Sequence EraseLineBackward; + extern const Sequence EraseLineEntirely; + } } inline std::ostream& operator<<(std::ostream& o, const AppInstaller::CLI::VirtualTerminal::Sequence& s) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -59,7 +59,7 @@ namespace AppInstaller::CLI::Workflow sourceName = context.Args.GetArg(Execution::Args::Type::Source); } - std::shared_ptr<Repository::ISource> source = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1)); + std::shared_ptr<Repository::ISource> source = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1), true); if (!source) { diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -9,6 +9,7 @@ #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Management.Deployment.h> +#include <winrt/Windows.UI.ViewManagement.h> #include <wil/result_macros.h> diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -7,7 +7,7 @@ #include "Public/AppInstallerSHA256.h" #include "Public/AppInstallerStrings.h" -#define AICLI_TraceLoggingStringView(_sv_,_name_) TraceLoggingCountedString(_sv_.data(), static_cast<ULONG>(_sv_.size()), _name_) +#define AICLI_TraceLoggingStringView(_sv_,_name_) TraceLoggingCountedUtf8String(_sv_.data(), static_cast<ULONG>(_sv_.size()), _name_) // Helper to print a GUID std::ostream& operator<<(std::ostream& out, const GUID& guid)