Skip to content
Open
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 .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ swgus
SYD
SYG
systemnotsupported
tableoutput
Tagit
TARG
taskhostw
Expand Down
5 changes: 5 additions & 0 deletions doc/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ The WinGet MCP server's existing tools have been extended with new parameters to

The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` environment variables to authenticate GitHub API requests. This significantly increases the GitHub API rate limit, preventing failures in CI/CD pipelines. Use `-Verbose` to see which token is being used.

### Improved `list` output when redirected

- `winget list` (and similar table commands) no longer truncates output when stdout is redirected to a file or variable — column widths are now computed from the full result set.
- Spinner and progress bar output are suppressed when no console is attached, keeping redirected output clean.

## Bug Fixes

* Fixed the `useLatest` property in the DSC v3 `Microsoft.WinGet/Package` resource schema to emit a boolean default (`false`) instead of the incorrect string `"false"`.
Expand Down
19 changes: 17 additions & 2 deletions src/AppInstallerCLICore/ChannelStreams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ namespace AppInstaller::CLI::Execution
{
using namespace VirtualTerminal;

size_t GetConsoleWidth()
#ifndef AICLI_DISABLE_TEST_HOOKS
static std::optional<size_t>* s_consoleWidthOverride = nullptr;

void TestHook_SetConsoleWidth_Override(std::optional<size_t>* value)
{
s_consoleWidthOverride = value;
}
#endif

std::optional<size_t> GetConsoleWidth()
{
#ifndef AICLI_DISABLE_TEST_HOOKS
if (s_consoleWidthOverride)
{
return *s_consoleWidthOverride;
}
#endif
CONSOLE_SCREEN_BUFFER_INFO consoleInfo{};
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &consoleInfo))
{
return static_cast<size_t>(consoleInfo.dwSize.X);
}
else
{
return 120;
return std::nullopt;
}
}

Expand Down
13 changes: 11 additions & 2 deletions src/AppInstallerCLICore/ChannelStreams.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@
#include "VTSupport.h"
#include <winget/LocIndependent.h>

#include <optional>
#include <ostream>
#include <string>


namespace AppInstaller::CLI::Execution
{
// Gets the current console width.
size_t GetConsoleWidth();
// Gets the current console width, or std::nullopt if stdout is not attached to a console
// (e.g. redirected to a file or pipe). Callers that receive nullopt should not truncate output.
std::optional<size_t> GetConsoleWidth();

#ifndef AICLI_DISABLE_TEST_HOOKS
// Overrides the value returned by GetConsoleWidth(). Pass nullptr to remove the override.
// Pass a pointer to std::nullopt to simulate no console; pass a pointer to a value to
// simulate a console of that width.
void TestHook_SetConsoleWidth_Override(std::optional<size_t>* value);
#endif
Comment on lines +19 to +24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't put the hooks into the headers here; only the test headers.


// The base stream for all channels.
struct BaseStream
Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/ExecutionProgress.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ namespace AppInstaller::CLI::Execution
}
else
{
m_out << '\r' << std::string(GetConsoleWidth(), ' ') << '\r';
m_out << '\r' << std::string(GetConsoleWidth().value_or(0), ' ') << '\r';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent of other changes I think that it would be better to just \n. Since we should never hit this if progress is disabled whenever this is std::nullopt, more of a nit.

}
}

Expand Down
14 changes: 10 additions & 4 deletions src/AppInstallerCLICore/ExecutionReporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ namespace AppInstaller::CLI::Execution
m_out(outStream),
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);
// Only create spinner and progress bar when stdout is attached to a console.
// When output is redirected to a file or pipe, suppress all progress output
// so it does not appear in the redirected stream.
if (GetConsoleWidth().has_value())
{
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);
}
Expand Down Expand Up @@ -146,7 +152,7 @@ namespace AppInstaller::CLI::Execution
{
m_style = style;

if (m_channel == Channel::Output)
if (m_channel == Channel::Output && GetConsoleWidth().has_value())
{
auto sixelSupported = [&]() { return SixelsSupported(); };
m_spinner = IIndefiniteSpinner::CreateForStyle(*m_out, ConsoleModeRestore::Instance().IsVTEnabled(), style, sixelSupported);
Expand Down
34 changes: 16 additions & 18 deletions src/AppInstallerCLICore/TableOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ namespace AppInstaller::CLI::Execution
using header_t = std::array<Resource::LocString, FieldCount>;
using line_t = std::array<std::string, FieldCount>;

TableOutput(Reporter& reporter, header_t&& header, size_t sizingBuffer = 50) :
m_reporter(reporter), m_sizingBuffer(sizingBuffer)
TableOutput(Reporter& reporter, header_t&& header) :
m_reporter(reporter),
m_hasConsole(GetConsoleWidth().has_value())
{
for (size_t i = 0; i < FieldCount; ++i)
{
Expand All @@ -35,15 +36,11 @@ namespace AppInstaller::CLI::Execution
{
m_empty = false;

if (m_buffer.size() < m_sizingBuffer)
{
m_buffer.emplace_back(std::move(line));
}
else
{
EvaluateAndFlushBuffer();
OutputLineToStream(line);
}
// Always buffer every row so that column widths are computed from the full dataset
// before any output is written. This guarantees that the widest value in any column
// is always fully visible and columns are perfectly aligned, whether output goes to
// a console or is redirected. Complete() triggers the actual output.
m_buffer.emplace_back(std::move(line));
}

void Complete()
Expand Down Expand Up @@ -71,10 +68,10 @@ namespace AppInstaller::CLI::Execution

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

void EvaluateAndFlushBuffer()
{
Expand Down Expand Up @@ -127,13 +124,14 @@ namespace AppInstaller::CLI::Execution
totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? 1 : 0);
}

size_t consoleWidth = GetConsoleWidth();
auto consoleWidthOpt = 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)
// If there is a console and the total space would be too big, shrink columns.
// We don't want to use the last column, lest we auto-wrap.
// When there is no console (e.g. output redirected to a file), skip truncation entirely.
if (consoleWidthOpt && totalRequired >= *consoleWidthOpt)
{
size_t extra = (totalRequired - consoleWidth) + 1;
size_t extra = (totalRequired - *consoleWidthOpt) + 1;

while (extra)
{
Expand All @@ -151,7 +149,7 @@ namespace AppInstaller::CLI::Execution
extra -= 1;
}

totalRequired = consoleWidth - 1;
totalRequired = *consoleWidthOpt - 1;
}

// Header line
Expand Down
5 changes: 3 additions & 2 deletions src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <winget/SelfManagement.h>
#include <winget/PathTree.h>
#include <winrt/Microsoft.Management.Configuration.h>
#include <limits>

using namespace AppInstaller::CLI::Execution;
using namespace winrt::Microsoft::Management::Configuration;
Expand Down Expand Up @@ -356,7 +357,7 @@ namespace AppInstaller::CLI::Workflow
truncated = true;
}

if (Utility::LimitOutputLines(lines, GetConsoleWidth(), maxLines))
if (Utility::LimitOutputLines(lines, GetConsoleWidth().value_or(std::numeric_limits<size_t>::max()), maxLines))
{
truncated = true;
}
Expand Down Expand Up @@ -767,7 +768,7 @@ namespace AppInstaller::CLI::Workflow
if (messageData.ShowDescription && !description.empty())
{
constexpr size_t maximumDescriptionLines = 3;
size_t consoleWidth = GetConsoleWidth();
size_t consoleWidth = GetConsoleWidth().value_or(std::numeric_limits<size_t>::max());
std::vector<std::string> lines = Utility::SplitIntoLines(description, maximumDescriptionLines + 1);
bool wasLimited = Utility::LimitOutputLines(lines, consoleWidth, maximumDescriptionLines);

Expand Down
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Workflows/WorkflowBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ namespace AppInstaller::CLI::Workflow
{
// Using a height of 4 arbitrarily; allow width up to the entire console.
UINT imageHeightCells = 4;
UINT imageWidthCells = static_cast<UINT>(Execution::GetConsoleWidth());
UINT imageWidthCells = static_cast<UINT>(Execution::GetConsoleWidth().value_or(120));

icon.RenderSizeInCells(imageWidthCells, imageHeightCells);
icon.RenderTo(outputStream);
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@
<ClCompile Include="SQLiteIndex.cpp" />
<ClCompile Include="SQLiteWrapper.cpp" />
<ClCompile Include="Synchronization.cpp" />
<ClCompile Include="TableOutput.cpp" />
<ClCompile Include="TestCommon.cpp" />
<ClCompile Include="WorkflowCommon.cpp" />
<ClCompile Include="WorkflowGroupPolicy.cpp" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@
<ClCompile Include="Synchronization.cpp">
<Filter>Source Files\Common</Filter>
</ClCompile>
<ClCompile Include="TableOutput.cpp">
<Filter>Source Files\Common</Filter>
</ClCompile>
<ClCompile Include="TestRestRequestHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
Expand Down
Loading