Skip to content

Instantly share code, notes, and snippets.

@a-luna
Forked from DanielSWolf/Program.cs
Last active April 16, 2021 04:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-luna/a2c44a29cdd22792939f8b406f971d9d to your computer and use it in GitHub Desktop.
Save a-luna/a2c44a29cdd22792939f8b406f971d9d to your computer and use it in GitHub Desktop.
Progress bar for console applications, added the ability to customize the various components of the progress bar. Added the ability to display or hide the progress bar, percent complete and animation. And just for fun, I created a set of animation sequences which can be used instead of the default "|/-\-" progress indicator.
namespace AaronLuna.Common.Console
{
using System;
using System.Linq;
using System.Text;
using System.Threading;
public class ConsoleProgressBar : IDisposable, IProgress<double>
{
readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 8);
internal Timer _timer;
internal double _currentProgress;
internal bool _disposed;
internal int _animationIndex;
string _currentText = string.Empty;
public ConsoleProgressBar()
{
Initialize();
NumberOfBlocks = 10;
StartBracket = "[";
EndBracket = "]";
CompletedBlock = "#";
IncompleteBlock = "-";
AnimationSequence = ProgressAnimations.Default;
DisplayBar = true;
DisplayPercentComplete = true;
DisplayAnimation = true;
}
public int NumberOfBlocks { get; set; }
public string StartBracket { get; set; }
public string EndBracket { get; set; }
public string CompletedBlock { get; set; }
public string IncompleteBlock { get; set; }
public string AnimationSequence { get; set; }
public bool DisplayBar { get; set; }
public bool DisplayPercentComplete { get; set; }
public bool DisplayAnimation { get; set; }
public void Report(double value)
{
// Make sure value is in [0..1] range
value = Math.Max(0, Math.Min(1, value));
Interlocked.Exchange(ref _currentProgress, value);
}
void Initialize()
{
_timer = new Timer(TimerHandler);
// A progress bar is only for temporary display in a console window.
// If the console output is redirected to a file, draw nothing.
// Otherwise, we'll end up with a lot of garbage in the target file.
if (!Console.IsOutputRedirected)
{
ResetTimer();
}
}
void TimerHandler(object state)
{
lock (_timer)
{
if (_disposed) return;
UpdateText(GetProgressBarText(_currentProgress));
ResetTimer();
}
}
string GetProgressBarText(double currentProgress)
{
var numBlocksCompleted = (int)(currentProgress * NumberOfBlocks);
var completedBlocks = string.Empty;
foreach (var i in Enumerable.Range(0, numBlocksCompleted))
{
completedBlocks += CompletedBlock;
}
var incompleteBlocks = string.Empty;
foreach (var i in Enumerable.Range(0, NumberOfBlocks - numBlocksCompleted))
{
incompleteBlocks += IncompleteBlock;
}
var progressBar = $"{StartBracket}{completedBlocks}{incompleteBlocks}{EndBracket} ";
var percent = $" {(int)(currentProgress * 100)}% ";
var whiteSpace = " ";
var animation = AnimationSequence[_animationIndex++ % AnimationSequence.Length];
if (!DisplayBar) progressBar = string.Empty;
if (!DisplayPercentComplete) percent = string.Empty;
if (!DisplayAnimation) animation = ' ';
if (currentProgress is 1)
{
animation = ' ';
}
var fullBar = $"{progressBar}{percent}{whiteSpace}{animation}{whiteSpace}";
fullBar = fullBar.Replace(" ", " ");
fullBar.TrimEnd();
return fullBar;
}
internal void UpdateText(string text)
{
// Get length of common portion
var commonPrefixLength = 0;
var commonLength = Math.Min(_currentText.Length, text.Length);
while (commonPrefixLength < commonLength && text[commonPrefixLength] == _currentText[commonPrefixLength])
{
commonPrefixLength++;
}
// Backtrack to the first differing character
var outputBuilder = new StringBuilder();
outputBuilder.Append('\b', _currentText.Length - commonPrefixLength);
// Output new suffix
outputBuilder.Append(text.Substring(commonPrefixLength));
// If the new text is shorter than the old one: delete overlapping characters
var overlapCount = _currentText.Length - text.Length;
if (overlapCount > 0)
{
outputBuilder.Append(' ', overlapCount);
outputBuilder.Append('\b', overlapCount);
}
//Console.Write($"{Caption}{outputBuilder}");
Console.Write(outputBuilder);
_currentText = text;
}
internal void ResetTimer()
{
_timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1));
}
protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
lock (_timer)
{
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
using System;
using System.Threading.Tasks;
using AaronLuna.Common.Console;
class Program
{
// async Main is a C# 7.1 feature, change your project settings to the
// new version if this is flagged as an error
static async Task Main(string[] args)
{
var pb1 = new ConsoleProgressBar();
await TestProgressBar(pb1);
var pb2 = new ConsoleProgressBar
{
NumberOfBlocks = 13,
StartBracket = "|",
EndBracket = "|",
CompletedBlock = "|",
IncompleteBlock = "\u00a0",
AnimationSequence = ProgressAnimations.Braille
};
await TestProgressBar(pb2);
var pb3 = new ConsoleProgressBar
{
StartBracket = string.Empty,
EndBracket = string.Empty,
CompletedBlock = "\u00bb",
IncompleteBlock = "-",
DisplayAnimation = false
};
await TestProgressBar(pb3);
var pb4 = new ConsoleProgressBar
{
DisplayBar = false,
AnimationSequence = ProgressAnimations.GrowingBar
};
await TestProgressBar(pb4);
}
static async Task TestProgressBar(ConsoleProgressBar progress)
{
Console.Write("Performing some task... ");
using (progress)
{
for (int i = 0; i <= 100; i++)
{
progress.Report((double)i / 100);
await Task.Delay(30);
}
progress.Report(1);
await Task.Delay(100);
}
Console.WriteLine("Done.");
}
}
// Example Output:
// Performing some task... [#---------] 12% \
// Performing some task... |||||||||-----| 62% ⠄
// Performing some task... »»»»------ 46%
// Performing some task... 92% ▆
//
// Performing some task... [##########] 100% Done.
// Performing some task... ||||||||||||||| 100% Done.
// Performing some task... »»»»»»»»»» 100% Done.
// Performing some task... 100% Done.
namespace AaronLuna.Common.Console
{
public static class ProgressAnimations
{
public const string Default = @"|/-\-";
public const string BouncingBall = ".oO\u00b0Oo.";
public const string Explosion = ".oO@*";
public const string RotatingArrow = "\u2190\u2196\u2191\u2197\u2192\u2198\u2193\u2199";
public const string GrowingBar = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2587\u2586\u2585\u2584\u2583\u2581";
public const string Braille = "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808";
public const string Semicircle = "\u25d0\u25d3\u25d1\u25d2";
public const string RotatingTriangle = "\u25e2\u25e3\u25e4\u25e5";
public const string RotatingSquare = "\u2596\u2598\u259d\u2597";
public const string RotatingPipe = "\u2524\u2518\u2534\u2514\u251c\u250c\u252c\u2510";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment