using System; using System.Text; using System.Threading; using Quadarax.Foundation.Core.Object; // ReSharper disable InconsistentNaming namespace Quadarax.Foundation.Core.Console { public class ProgressBar : DisposableObject, IProgress { #region *** Constants *** private const int CN_BLOCK_COUNT = 10; private const string CS_ANIMATION = @"|/-\"; #endregion #region *** Private fields *** private readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 8); private readonly Timer _timer; private double _currentProgress; private string _currentText = string.Empty; private int _animationIndex; private ConsoleWriter? _writer; #endregion #region *** Constructors *** public ProgressBar(ConsoleWriter writer) { _writer = writer ?? throw new ArgumentNullException(nameof(writer)); _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 (!System.Console.IsOutputRedirected) { ResetTimer(); } } #endregion #region *** Public Operations *** /// /// Update progress status. /// /// Status value between 0 - 1. Represents percentage. 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); } #endregion #region *** Private Operations *** private void TimerHandler(object? state) { lock (_timer) { var progressBlockCount = (int)(_currentProgress * CN_BLOCK_COUNT); var percent = (int)(_currentProgress * 100); var text = $"[{new string('#', progressBlockCount)}{new string('-', CN_BLOCK_COUNT - progressBlockCount)}] {percent,3}% {CS_ANIMATION[_animationIndex++ % CS_ANIMATION.Length]}"; UpdateText(text); ResetTimer(); } } private 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); } _writer?.Write(outputBuilder.ToString()); _currentText = text; } private void ResetTimer() { _timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1)); } #endregion #region *** Private Overrides *** protected override void OnDisposing() { lock (_timer) { UpdateText(string.Empty); _writer = null; } } #endregion } }