diff --git a/src/ShellProgressBar.Example/Examples/MessageBeforeAndAfterExample.cs b/src/ShellProgressBar.Example/Examples/MessageBeforeAndAfterExample.cs index 4280eef..eaaf98e 100644 --- a/src/ShellProgressBar.Example/Examples/MessageBeforeAndAfterExample.cs +++ b/src/ShellProgressBar.Example/Examples/MessageBeforeAndAfterExample.cs @@ -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, @@ -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"); }); } diff --git a/src/ShellProgressBar.Example/Program.cs b/src/ShellProgressBar.Example/Program.cs index b947538..0acc86e 100644 --- a/src/ShellProgressBar.Example/Program.cs +++ b/src/ShellProgressBar.Example/Program.cs @@ -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); @@ -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++; } diff --git a/src/ShellProgressBar/ProgressBar.cs b/src/ShellProgressBar/ProgressBar.cs index a76e525..9208fa8 100644 --- a/src/ShellProgressBar/ProgressBar.cs +++ b/src/ShellProgressBar/ProgressBar.cs @@ -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; @@ -15,11 +14,10 @@ public class ProgressBar : ProgressBarBase, IProgressBar private readonly ConsoleColor _originalColor; private readonly Func _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; @@ -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 @@ -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) => @@ -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); @@ -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; @@ -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 children, Indentation[] indentation, @@ -392,12 +422,12 @@ private static void DrawChildren(IEnumerable 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; @@ -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); } diff --git a/src/ShellProgressBar/StringExtensions.cs b/src/ShellProgressBar/StringExtensions.cs index bc3071b..0d00102 100644 --- a/src/ShellProgressBar/StringExtensions.cs +++ b/src/ShellProgressBar/StringExtensions.cs @@ -12,5 +12,32 @@ public static string Excerpt(string phrase, int length = 60) return phrase; return phrase.Substring(0, length - 3) + "..."; } + + /// + /// 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. + /// + /// + /// + public static IEnumerable 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; + } + } + } } }