Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes scrollable windows console and writing multiple lines in single… #103

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
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
9 changes: 8 additions & 1 deletion src/ShellProgressBar.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -88,12 +91,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
98 changes: 64 additions & 34 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 @@ -345,7 +345,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 +360,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 +422,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 +530,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
27 changes: 27 additions & 0 deletions src/ShellProgressBar/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,32 @@ public static string Excerpt(string phrase, int length = 60)
return phrase;
return phrase.Substring(0, length - 3) + "...";
}

/// <summary>
/// Splits a string into it's indiviudal lines and then again splits these individual lines
/// into multiple lines if they exceed the width of the console.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static IEnumerable<string> SplitToConsoleLines(this string str)
{
int width = Console.BufferWidth;
var lines = str.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

foreach (var line in lines)
{
if (line.Length > width)
{
for (int i = 0; i < line.Length; i += width)
{
yield return line.Substring(i, Math.Min(width, line.Length - i));
}
}
else
{
yield return line;
}
}
}
}
}