Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
<ClInclude Include="pch.h" />
<ClInclude Include="Public\AppInstallerCLICore.h" />
<ClInclude Include="Search\Search.h" />
<ClInclude Include="TableOutput.h" />
<ClInclude Include="VTSupport.h" />
<ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" />
<ClInclude Include="Workflows\InstallFlow.h" />
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@
<ClInclude Include="Commands\ValidateCommand.h">
<Filter>Commands</Filter>
</ClInclude>
<ClInclude Include="TableOutput.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
Expand Down
208 changes: 208 additions & 0 deletions src/AppInstallerCLICore/TableOutput.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
#include "ExecutionReporter.h"

#include <array>
#include <ostream>
#include <string>
#include <vector>


namespace AppInstaller::CLI::Execution
{
namespace details
{
// Gets the column width of the console.
inline size_t GetConsoleWidth()
{
CONSOLE_SCREEN_BUFFER_INFO consoleInfo{};
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &consoleInfo))
{
return static_cast<size_t>(consoleInfo.dwSize.X);
}
else
{
return 120;
}
}
}

// Enables output data in a table format.
template <size_t FieldCount>
struct TableOutput
{
using line_t = std::array<std::string, FieldCount>;

TableOutput(Reporter& reporter, line_t&& header, size_t sizingBuffer = 50) :
m_reporter(reporter), m_sizingBuffer(sizingBuffer)
{
for (size_t i = 0; i < FieldCount; ++i)
{
m_columns[i].Name = std::move(header[i]);
m_columns[i].MinLength = m_columns[i].Name.length();
m_columns[i].MaxLength = 0;
}
}

void OutputLine(line_t&& line)
{
if (m_buffer.size() < m_sizingBuffer)
{
m_buffer.emplace_back(std::move(line));
}
else
{
EvaulateAndFlushBuffer();
OutputLineToStream(line);
}
}

void Complete()
{
EvaulateAndFlushBuffer();
}

private:
// A column in the table.
struct Column
{
std::string Name;
size_t MinLength = 0;
size_t MaxLength = 0;
bool SpaceAfter = true;
};

Reporter& m_reporter;
std::array<Column, FieldCount> m_columns;
size_t m_sizingBuffer;
std::vector<line_t> m_buffer;
bool m_bufferEvaluated = false;

void EvaulateAndFlushBuffer()
{
if (m_bufferEvaluated)
{
return;
}

// Determine the maximum length for all columns
for (const auto& line : m_buffer)
{
for (size_t i = 0; i < FieldCount; ++i)
{
m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, line[i].length());
}
}

// Only output the extra space if:
// 1. Not the last field
m_columns[FieldCount - 1].SpaceAfter = false;

// 2. Not empty (taken care of by not doing anything if empty)
// 3. There are non-empty fields after
for (size_t i = FieldCount - 1; i > 0; --i)
{
if (m_columns[i].MaxLength)
{
break;
}
else
{
m_columns[i - 1].SpaceAfter = false;
}
}

// Determine the total width required to not truncate any columns
size_t totalRequired = 0;

for (size_t i = 0; i < FieldCount; ++i)
{
totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? 1 : 0);
}

size_t consoleWidth = details::GetConsoleWidth();

// If the total space would be too big, shrink them.
// We don't want to use the last column, lest we auto-wrap
if (totalRequired >= consoleWidth)
{
size_t extra = (totalRequired - consoleWidth) + 1;

while (extra)
{
size_t targetIndex = 0;
size_t targetVal = m_columns[0].MaxLength;
for (size_t j = 1; j < FieldCount; ++j)
{
if (m_columns[j].MaxLength > targetVal)
{
targetIndex = j;
targetVal = m_columns[j].MaxLength;
}
}
m_columns[targetIndex].MaxLength -= 1;
extra -= 1;
}

totalRequired = consoleWidth - 1;
}

// Header line
line_t headerLine;

for (size_t i = 0; i < FieldCount; ++i)
{
headerLine[i] = m_columns[i].Name;
}

OutputLineToStream(headerLine);

m_reporter.Info() << std::string(totalRequired, '-') << std::endl;

for (const auto& line : m_buffer)
{
OutputLineToStream(line);
}

m_bufferEvaluated = true;
}

void OutputLineToStream(const line_t& line)
{
auto out = m_reporter.Info();

for (size_t i = 0; i < FieldCount; ++i)
{
const auto& col = m_columns[i];

if (col.MaxLength)
{
if (line[i].length() > col.MaxLength)
{
size_t replaceChars = std::min(col.MaxLength, static_cast<size_t>(3));
std::string replacement = line[i].substr(0, col.MaxLength - replaceChars);
replacement.append(replaceChars, '.');
out << replacement;

if (col.SpaceAfter)
{
out << ' ';
}
}
else
{
out << line[i];

if (col.SpaceAfter)
{
out << std::string(col.MaxLength - line[i].length() + 1, ' ');
}
}
}
}

out << std::endl;
}
};
}
39 changes: 30 additions & 9 deletions src/AppInstallerCLICore/Workflows/WorkflowBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,31 @@
#include "WorkflowBase.h"
#include "ExecutionContext.h"
#include "ManifestComparator.h"
#include "TableOutput.h"


namespace AppInstaller::CLI::Workflow
{
using namespace AppInstaller::Repository;

namespace
{
std::string GetMatchCriteriaDescriptor(const ResultMatch& match)
{
if (match.MatchCriteria.Field != ApplicationMatchField::Id && match.MatchCriteria.Field != ApplicationMatchField::Name)
{
std::string result{ ApplicationMatchFieldToString(match.MatchCriteria.Field) };
result += ": ";
result += match.MatchCriteria.Value;
return result;
}
else
{
return {};
}
}
}

bool WorkflowTask::operator==(const WorkflowTask& other) const
{
if (m_isFunc && other.m_isFunc)
Expand Down Expand Up @@ -133,20 +152,22 @@ namespace AppInstaller::CLI::Workflow
{
auto& searchResult = context.Get<Execution::Data::SearchResult>();
Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size());
for (auto& match : searchResult.Matches)

Execution::TableOutput<4> table(context.Reporter, { "Name", "Id", "Version", "Matched" });

for (size_t i = 0; i < searchResult.Matches.size(); ++i)
{
auto app = match.Application.get();
auto app = searchResult.Matches[i].Application.get();
auto allVersions = app->GetVersions();

// Assume versions are sorted when returned so we'll use the first one as the latest version
context.Reporter.Info() << app->GetId() << ", " << app->GetName() << ", " << allVersions.at(0).GetVersion().ToString();
table.OutputLine({ app->GetName(), app->GetId(), allVersions.at(0).GetVersion().ToString(), GetMatchCriteriaDescriptor(searchResult.Matches[i]) });
}

if (match.MatchCriteria.Field != ApplicationMatchField::Id && match.MatchCriteria.Field != ApplicationMatchField::Name)
{
context.Reporter.Info() << ", [" << ApplicationMatchFieldToString(match.MatchCriteria.Field) << ": " << match.MatchCriteria.Value << "]";
}
table.Complete();

context.Reporter.Info() << std::endl;
if (searchResult.Truncated)
{
context.Reporter.Info() << "<additional entries truncated due to result limit>" << std::endl;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@

#include <wil/result_macros.h>

#include <array>
#include <iostream>
#include <fstream>
#include <future>
#include <functional>
#include <memory>
#include <numeric>
#include <optional>
#include <sstream>
#include <string_view>
Expand Down
Loading