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
138 changes: 101 additions & 37 deletions src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ namespace Microsoft.PowerShell

this.Style = IsMinimalProgressRenderingEnabled()
? RenderStyle.Ansi
: this.Style = RenderStyle.FullPlus;
: RenderStyle.FullPlus;

this.SourceId = sourceId;
}
Expand Down Expand Up @@ -111,7 +111,7 @@ namespace Microsoft.PowerShell
RenderMinimal(strCollection, indentation, maxWidth, rawUI);
break;
case RenderStyle.Ansi:
RenderAnsi(strCollection, indentation, maxWidth);
RenderAnsi(strCollection, indentation, maxWidth, rawUI);
break;
case RenderStyle.Invisible:
// do nothing
Expand Down Expand Up @@ -368,9 +368,12 @@ internal static bool IsMinimalProgressRenderingEnabled()
/// <param name="maxWidth">
/// The maximum number of chars that the rendering is allowed to consume.
/// </param>
/// <param name="rawUI">
/// The PSHostRawUserInterface used to gauge string widths in the rendering.
/// </param>
private
void
RenderAnsi(ArrayList strCollection, int indentation, int maxWidth)
RenderAnsi(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
{
string indent = StringUtil.Padding(indentation);
string secRemain = string.Empty;
Expand All @@ -389,68 +392,129 @@ internal static bool IsMinimalProgressRenderingEnabled()

// if the activity is really long, only use up to half the width
string activity;
if (Activity.Length > maxWidth / 2)
int activityDisplayCellsWidth = rawUI.LengthInBufferCells(Activity);
if (activityDisplayCellsWidth > maxWidth / 2)
{
activity = Activity.Substring(0, maxWidth / 2) + PSObjectHelper.Ellipsis;
activity = StringUtil.TruncateToBufferCellWidth(rawUI, Activity, (maxWidth / 2) - 1) + PSObjectHelper.Ellipsis;
}
else
{
activity = Activity;
}

activityDisplayCellsWidth = rawUI.LengthInBufferCells(activity);

// 4 is for the extra space and square brackets below and one extra space
int barWidth = maxWidth - activity.Length - indentation - 4;
int barWidth = maxWidth - activityDisplayCellsWidth - indentation - 4;

var sb = new StringBuilder();
int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length;
sb.Append(PSStyle.Instance.Reverse);

int maxStatusLength = barWidth - secRemainLength - 1;
if (maxStatusLength > 0 && StatusDescription.Length > barWidth - secRemainLength)
// Build the status description part
int maxStatusWidth = barWidth - secRemainLength;
string statusPart = RenderAnsiStatusPart(rawUI, maxStatusWidth, out int statusPartDisplayWidth);

sb.Append(statusPart);

// Calculate padding needed
int emptyPadLength = barWidth - statusPartDisplayWidth - secRemainLength;
if (emptyPadLength > 0)
{
sb.Append(StatusDescription.AsSpan(0, barWidth - secRemainLength - 1));
sb.Append(PSObjectHelper.Ellipsis);
sb.Append(' ', emptyPadLength);
}
else

sb.Append(secRemain);

// Insert ReverseOff at the correct position for the progress bar
RenderAnsiReverseOff(sb, rawUI, statusPart, barWidth);

strCollection.Add(
StringUtil.Format(
"{0}{1}{2} [{3}]{4}",
indent,
PSStyle.Instance.Progress.Style,
activity,
sb.ToString(),
PSStyle.Instance.Reset));
}

/// <summary>
/// Builds the status-description portion of the Ansi progress bar, truncating it
/// with an ellipsis when it would exceed the available status width.
/// </summary>
/// <param name="rawUI">
/// The PSHostRawUserInterface used to gauge string widths.
/// </param>
/// <param name="maxStatusWidth">
/// The maximum number of buffer cells available for the status description.
/// </param>
/// <param name="statusPartDisplayWidth">
/// On return, the width in buffer cells of the produced status part.
/// </param>
/// <returns>The status description, possibly truncated with an ellipsis.</returns>
private string RenderAnsiStatusPart(PSHostRawUserInterface rawUI, int maxStatusWidth, out int statusPartDisplayWidth)
{
int statusDisplayWidth = rawUI.LengthInBufferCells(StatusDescription);

if (maxStatusWidth <= 0 || statusDisplayWidth <= maxStatusWidth)
{
sb.Append(StatusDescription);
statusPartDisplayWidth = statusDisplayWidth;
return StatusDescription;
}

int emptyPadLength = barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength;
if (emptyPadLength > 0)
int ellipsisWidth = rawUI.LengthInBufferCells(PSObjectHelper.EllipsisStr);
string statusPart = StringUtil.TruncateToBufferCellWidth(rawUI, StatusDescription, maxStatusWidth - ellipsisWidth) + PSObjectHelper.EllipsisStr;
statusPartDisplayWidth = rawUI.LengthInBufferCells(statusPart);
return statusPart;
}

/// <summary>
/// Appends the ReverseOff VT sequence to the rendered bar at the buffer-cell position
/// that corresponds to the filled portion of the progress bar, respecting character boundaries.
/// </summary>
/// <param name="sb">The StringBuilder holding the bar contents built so far.</param>
/// <param name="rawUI">
/// The PSHostRawUserInterface used to gauge string widths.
/// </param>
/// <param name="statusPart">The status text at the start of the bar.</param>
/// <param name="barWidth">The total width of the progress bar in buffer cells.</param>
private void RenderAnsiReverseOff(StringBuilder sb, PSHostRawUserInterface rawUI, string statusPart, int barWidth)
{
if (PercentComplete < 0 || PercentComplete >= 100 || barWidth <= 0)
{
sb.Append(string.Empty.PadRight(emptyPadLength));
sb.Append(PSStyle.Instance.ReverseOff);
return;
}

sb.Append(secRemain);
int barLength = PercentComplete * barWidth / 100;
if (barLength >= barWidth)
{
barLength = barWidth - 1;
}

// Calculate the string position where we need to insert ReverseOff.
// We need to find the character position that corresponds to barLength buffer cells.
int stringPos = PSStyle.Instance.Reverse.Length;
int currentCellCount = 0;

if (PercentComplete >= 0 && PercentComplete < 100 && barWidth > 0)
for (int i = 0; i < statusPart.Length && currentCellCount < barLength; i++)
{
int barLength = PercentComplete * barWidth / 100;
if (barLength >= barWidth)
{
barLength = barWidth - 1;
}
currentCellCount += rawUI.LengthInBufferCells(statusPart[i]);
stringPos++;
}

if (barLength < sb.Length)
{
sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff);
}
// Add any padding characters.
int remainingCells = barLength - currentCellCount;
stringPos += Math.Max(0, remainingCells);

if (stringPos < sb.Length)
{
sb.Insert(stringPos, PSStyle.Instance.ReverseOff);
}
else
{
sb.Append(PSStyle.Instance.ReverseOff);
}

strCollection.Add(
StringUtil.Format(
"{0}{1}{2} [{3}]{4}",
indent,
PSStyle.Instance.Progress.Style,
activity,
sb.ToString(),
PSStyle.Instance.Reset)
.PadRight(padding));
}

/// <summary>
Expand Down
56 changes: 56 additions & 0 deletions test/xUnit/csharp/TestProgressRawUI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Management.Automation.Host;
using Microsoft.PowerShell;

namespace PSTests.Parallel
{
/// <summary>
/// Test helper class that implements PSHostRawUserInterface for progress bar testing.
/// Delegates width calculations to ConsoleControl.LengthInBufferCells.
/// </summary>
internal sealed class TestProgressRawUI : PSHostRawUserInterface
{
public override ConsoleColor ForegroundColor { get; set; }

public override ConsoleColor BackgroundColor { get; set; }

public override Coordinates CursorPosition { get; set; }

public override Coordinates WindowPosition { get; set; }

public override int CursorSize { get; set; }

public override Size BufferSize { get; set; }

public override Size WindowSize { get; set; }

public override Size MaxWindowSize => new Size(120, 50);

public override Size MaxPhysicalWindowSize => new Size(120, 50);

public override string WindowTitle { get; set; }

public override bool KeyAvailable => false;

public override BufferCell[,] GetBufferContents(Rectangle rectangle) => throw new NotImplementedException();

public override void SetBufferContents(Rectangle rectangle, BufferCell fill) => throw new NotImplementedException();

public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) => throw new NotImplementedException();

public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) => throw new NotImplementedException();

public override KeyInfo ReadKey(ReadKeyOptions options) => throw new NotImplementedException();

public override void FlushInputBuffer() => throw new NotImplementedException();

public override int LengthInBufferCells(string str) => ConsoleControl.LengthInBufferCells(str, 0, checkEscapeSequences: true);

public override int LengthInBufferCells(string str, int offset) => ConsoleControl.LengthInBufferCells(str, offset, checkEscapeSequences: true);

public override int LengthInBufferCells(char c) => ConsoleControl.LengthInBufferCells(c);
}
}
Loading
Loading