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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ShellProgressBar.Example.Examples {
public class AutomaticEstimatedDurationExample : ExampleBase {
protected override Task StartAsync()
{
const int totalTicks = 10;
var options = new ProgressBarOptions
{
ProgressCharacter = '─',
AutomaticEstimatedDuration = true,
ShowEstimatedDuration = true
};
using (var pbar = new ProgressBar(totalTicks, "It can attempt to automatically calculate the duration as well", options))
{
var rand = new Random();

var initialMessage = pbar.Message;
for (var i = 0; i < totalTicks; i++)
{
pbar.Message = $"Start {i + 1} of {totalTicks}: {initialMessage}";
Thread.Sleep(rand.Next(400,1200));

pbar.Tick( $"End {i + 1} of {totalTicks}: {initialMessage}");
}
}

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class MessageBeforeAndAfterExample : ExampleBase
protected override Task StartAsync()
{
Console.WriteLine("This should not be overwritten");
const int totalTicks = 10;
int totalTicks = Console.WindowHeight;
var options = new ProgressBarOptions
{
ForegroundColor = ConsoleColor.Yellow,
Expand All @@ -18,9 +18,27 @@ protected override Task StartAsync()
};
using (var pbar = new ProgressBar(totalTicks, "showing off styling", options))
{
TickToCompletion(pbar, totalTicks, sleep: 500, i =>
TickToCompletion(pbar, totalTicks, sleep: 250, i =>
{
pbar.WriteErrorLine($"This should appear above:{i}");
if (i % 5 == 0)
{
// Single line
pbar.WriteErrorLine($"[{i}] This{Environment.NewLine}[{i}] is{Environment.NewLine}[{i}] over{Environment.NewLine}[{i}] 4 lines");
return;
}
if (i % 4 == 0)
{
// Single line
pbar.WriteErrorLine($"[{i}] This has{Environment.NewLine}[{i}] 2 lines.");
return;
}
if (i % 3 == 0)
{
// Single line
pbar.WriteErrorLine($"[{i}] This is a very long line {new string('.', Console.BufferWidth)} and should be split over 2 lines");
return;
}
pbar.WriteErrorLine($"[{i}] This should appear above");
});
}

Expand Down
17 changes: 13 additions & 4 deletions src/ShellProgressBar.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ShellProgressBar.Example.Examples;
Expand Down Expand Up @@ -44,7 +43,8 @@ class Program
new DeeplyNestedProgressBarTreeExample(),
new EstimatedDurationExample(),
new DownloadProgressExample(),
new AlternateFinishedColorExample()
new AlternateFinishedColorExample(),
new AutomaticEstimatedDurationExample()
};

public static async Task Main(string[] args)
Expand All @@ -66,6 +66,9 @@ private static async Task MainAsync(string[] args, CancellationToken token)
case "test":
await RunTestCases(token);
return;
case "scrolltest":
await RunTestCases(token, Console.WindowHeight+5);
return;
case "example":
var nth = args.Length > 1 ? int.Parse(args[1]) : 0;
await RunExample(nth, token);
Expand All @@ -78,7 +81,9 @@ private static async Task MainAsync(string[] args, CancellationToken token)

private static async Task RunExample(int nth, CancellationToken token)
{
if (nth > Examples.Count - 1 || nth < 0)
if (nth < 0)
nth = Examples.Count + nth;
if (nth > Examples.Count - 1)
{
await Console.Error.WriteLineAsync($"There are only {Examples.Count} examples, {nth} is not valid");
}
Expand All @@ -88,12 +93,16 @@ private static async Task RunExample(int nth, CancellationToken token)
await example.Start(token);
}

private static async Task RunTestCases(CancellationToken token)
private static async Task RunTestCases(CancellationToken token, int writeNumOfRowBefore = 0)
{
var i = 0;
foreach (var example in TestCases)
{
if (i > 0) Console.Clear(); //not necessary but for demo/recording purposes.

for (int r = 0; r< writeNumOfRowBefore; r++)
Console.WriteLine($"Writing output before test. Row {r+1}/{writeNumOfRowBefore}");

await example.Start(token);
i++;
}
Expand Down
3 changes: 1 addition & 2 deletions src/ShellProgressBar.Example/ShellProgressBar.Example.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--<Import Project="..\..\build\Versioning.targets" />-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>shellprogressbar-example</AssemblyName>
<RootNamespace>ShellProgressBar.Example</RootNamespace>
<Version>$(CurrentVersion)</Version>
Expand Down
116 changes: 72 additions & 44 deletions src/ShellProgressBar/ProgressBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -15,11 +14,10 @@ public class ProgressBar : ProgressBarBase, IProgressBar

private readonly ConsoleColor _originalColor;
private readonly Func<ConsoleOutLine, int> _writeMessageToConsole;
private readonly int _originalWindowTop;
private readonly int _originalWindowHeight;
private readonly bool _startedRedirected;
private int _originalCursorTop;
private int _isDisposed;
private int _lastDrawBottomPos;

private Timer _timer;
private int _visibleDescendants = 0;
Expand All @@ -41,8 +39,6 @@ public ProgressBar(int maxTicks, string message, ProgressBarOptions options = nu
try
{
_originalCursorTop = Console.CursorTop;
_originalWindowTop = Console.WindowTop;
_originalWindowHeight = Console.WindowHeight + _originalWindowTop;
_originalColor = Console.ForegroundColor;
}
catch
Expand All @@ -56,7 +52,7 @@ public ProgressBar(int maxTicks, string message, ProgressBarOptions options = nu
if (this.Options.EnableTaskBarProgress)
TaskbarProgress.SetState(TaskbarProgress.TaskbarStates.Normal);

if (this.Options.DisplayTimeInRealTime)
if (this.Options.DisplayTimeInRealTime)
_timer = new Timer((s) => OnTimerTick(), null, 500, 500);
else //draw once
_timer = new Timer((s) =>
Expand Down Expand Up @@ -102,18 +98,22 @@ protected override void Grow(ProgressBarHeight direction)

private void EnsureMainProgressBarVisible(int extraBars = 0)
{
var lastVisibleRow = Console.WindowHeight + Console.WindowTop;

var pbarHeight = this.Options.DenseProgressBar ? 1 : 2;
var neededPadding = Math.Min(_originalWindowHeight - pbarHeight, (1 + extraBars) * pbarHeight);
var difference = _originalWindowHeight - _originalCursorTop;
var write = difference <= neededPadding ? Math.Max(0, Math.Max(neededPadding, difference)) : 0;
var neededPadding = Math.Min(lastVisibleRow - pbarHeight, (1 + extraBars) * pbarHeight);
var difference = lastVisibleRow - _originalCursorTop;
var write = difference <= neededPadding ? Math.Min(Console.WindowHeight, Math.Max(0, Math.Max(neededPadding, difference))) : 0;

if (write == 0)
return;

var written = 0;
for (; written < write; written++)
Console.WriteLine();
if (written == 0) return;

Console.CursorTop = _originalWindowHeight - (written);
_originalCursorTop = Console.CursorTop - 1;
Console.CursorTop = Console.WindowHeight + Console.WindowTop - write;
_originalCursorTop = Console.CursorTop -1;
}

private void GrowDrawingAreaBasedOnChildren() => EnsureMainProgressBarVisible(_visibleDescendants);
Expand Down Expand Up @@ -181,8 +181,9 @@ private static void ProgressBarBottomHalf(double percentage, DateTime startDate,
durationString +=
$" / {GetDurationString(estimatedDuration)}";

var column1Width = Console.WindowWidth - durationString.Length - (depth * 2) + 2;
var column2Width = durationString.Length;
int durationStringWidth = durationString.GetColumnWidth();
//assume: depth char column width
var column1Width = Console.WindowWidth - durationStringWidth - (depth * 2) + 2;

if (!string.IsNullOrWhiteSpace(ProgressBarOptions.ProgressMessageEncodingName))
{
Expand All @@ -194,17 +195,14 @@ private static void ProgressBarBottomHalf(double percentage, DateTime startDate,
else
DrawBottomHalfPrefix(indentation, depth);

var format = $"{{0, -{column1Width}}}{{1,{column2Width}}}";
var percentageFormatedString = string.Format(percentageFormat, percentage);
var truncatedMessage = StringExtensions.Excerpt(percentageFormatedString + message, column1Width);

if (disableBottomPercentage)
if (!disableBottomPercentage)
{
truncatedMessage = StringExtensions.Excerpt(message, column1Width);
var percentageFormatedString = string.Format(percentageFormat, percentage);
message = percentageFormatedString + message;

}

var formatted = string.Format(format, truncatedMessage, durationString);
var m = formatted + new string(' ', Math.Max(0, maxCharacterWidth - formatted.Length));
string m = StringExtensions.TurncatedRightPad(message, column1Width) + durationString;
Console.Write(m);
}

Expand Down Expand Up @@ -345,7 +343,12 @@ void TopHalf()

DrawChildren(this.Children, indentation, ref cursorTop, Options.PercentageFormat);

ResetToBottom(ref cursorTop);
if (Console.CursorTop < _lastDrawBottomPos)
{
// The bar shrunk. Need to clean the remaining rows
ClearLines(_lastDrawBottomPos - Console.CursorTop);
}
_lastDrawBottomPos = Console.CursorTop;

Console.SetCursorPosition(0, _originalCursorTop);
Console.ForegroundColor = _originalColor;
Expand All @@ -355,35 +358,60 @@ void TopHalf()
_timer = null;
}

private static void ClearLines(int numOfLines)
{
// Use bufferwidth and not only the visible width. (currently identical on all platforms)
Console.Write(new string(' ', Console.BufferWidth * numOfLines));
}

private static string _resetString = "";
private static void ClearCurrentLine()
{
if (_resetString.Length != Console.BufferWidth + 2)
{
// Use buffer width and not only the visible width. (currently identical on all platforms)
_resetString = $"\r{new string(' ', Console.BufferWidth)}\r";
}
Console.Write(_resetString);
}

private void WriteConsoleLine(ConsoleOutLine m)
{
var resetString = new string(' ', Console.WindowWidth);
Console.Write(resetString);
Console.Write("\r");
var foreground = Console.ForegroundColor;
var background = Console.BackgroundColor;
var written = _writeMessageToConsole(m);
ClearCurrentLine();
var moved = _writeMessageToConsole(m);
Console.ForegroundColor = foreground;
Console.BackgroundColor = background;
_originalCursorTop += written;
_originalCursorTop += moved;
}

private static int DefaultConsoleWrite(ConsoleOutLine line)
{
if (line.Error) Console.Error.WriteLine(line.Line);
else Console.WriteLine(line.Line);
return 1;
}
var fromPos = Console.CursorTop;

private void ResetToBottom(ref int cursorTop)
{
var resetString = new string(' ', Console.WindowWidth);
var windowHeight = _originalWindowHeight;
if (cursorTop >= (windowHeight - 1)) return;
do
// First line was already cleared by WriteConsoleLine().
// Would be cleaner to do it here, but would break backwards compatibility for those
// who implemented their own writer function.
bool isClearedLine = true;
foreach (var outLine in line.Line.SplitToConsoleLines())
{
Console.Write(resetString);
} while (++cursorTop < (windowHeight - 1));
// Skip slower line clearing if we scrolled on last write
if (!isClearedLine)
ClearCurrentLine();

int lastCursorTop = Console.CursorTop;
if (line.Error)
Console.Error.WriteLine(outLine);
else
Console.WriteLine(outLine);

// If the cursorTop is still on same position we are at the end of the buffer and scrolling happened.
isClearedLine = lastCursorTop == Console.CursorTop;
}

// Return how many rows the cursor actually moved by.
return Console.CursorTop - fromPos;
}

private static void DrawChildren(IEnumerable<ChildProgressBar> children, Indentation[] indentation,
Expand All @@ -392,12 +420,12 @@ private static void DrawChildren(IEnumerable<ChildProgressBar> children, Indenta
var view = children.Where(c => !c.Collapse).Select((c, i) => new {c, i}).ToList();
if (!view.Any()) return;

var windowHeight = Console.WindowHeight;
var lastVisibleRow = Console.WindowHeight + Console.WindowTop;
var lastChild = view.Max(t => t.i);
foreach (var tuple in view)
{
//Dont bother drawing children that would fall off the screen
if (cursorTop >= (windowHeight - 2))
// Dont bother drawing children that would fall off the screen and don't want to scroll top out of view
if (cursorTop >= (lastVisibleRow - 2))
return;

var child = tuple.c;
Expand Down Expand Up @@ -500,7 +528,7 @@ public void Dispose()
{
var pbarHeight = this.Options.DenseProgressBar ? 1 : 2;
var openDescendantsPadding = (_visibleDescendants * pbarHeight);
var newCursorTop = Math.Min(_originalWindowHeight, _originalCursorTop + pbarHeight + openDescendantsPadding);
var newCursorTop = Math.Min(Console.WindowHeight+Console.WindowTop, _originalCursorTop + pbarHeight + openDescendantsPadding);
Console.CursorVisible = true;
Console.SetCursorPosition(0, newCursorTop);
}
Expand Down
Loading