ProgressBar.cs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. using System;
  2. using System.Text;
  3. using System.Threading;
  4. using Quadarax.Foundation.Core.Object;
  5. // ReSharper disable InconsistentNaming
  6. namespace Quadarax.Foundation.Core.Console
  7. {
  8. public class ProgressBar : DisposableObject, IProgress<double>
  9. {
  10. #region *** Constants ***
  11. private const int CN_BLOCK_COUNT = 10;
  12. private const string CS_ANIMATION = @"|/-\";
  13. #endregion
  14. #region *** Private fields ***
  15. private readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 8);
  16. private readonly Timer _timer;
  17. private double _currentProgress;
  18. private string _currentText = string.Empty;
  19. private int _animationIndex;
  20. private ConsoleWriter _writer;
  21. #endregion
  22. #region *** Constructors ***
  23. public ProgressBar(ConsoleWriter writer)
  24. {
  25. _writer = writer ?? throw new ArgumentNullException(nameof(writer));
  26. _timer = new Timer(TimerHandler);
  27. // A progress bar is only for temporary display in a console window.
  28. // If the console output is redirected to a file, draw nothing.
  29. // Otherwise, we'll end up with a lot of garbage in the target file.
  30. if (!System.Console.IsOutputRedirected)
  31. {
  32. ResetTimer();
  33. }
  34. }
  35. #endregion
  36. #region *** Public Operations ***
  37. /// <summary>
  38. /// Update progress status.
  39. /// </summary>
  40. /// <param name="value">Status value between 0 - 1. Represents percentage.</param>
  41. public void Report(double value)
  42. {
  43. // Make sure value is in [0..1] range
  44. value = Math.Max(0, Math.Min(1, value));
  45. Interlocked.Exchange(ref _currentProgress, value);
  46. }
  47. #endregion
  48. #region *** Private Operations ***
  49. private void TimerHandler(object state)
  50. {
  51. lock (_timer)
  52. {
  53. var progressBlockCount = (int)(_currentProgress * CN_BLOCK_COUNT);
  54. var percent = (int)(_currentProgress * 100);
  55. var text = $"[{new string('#', progressBlockCount)}{new string('-', CN_BLOCK_COUNT - progressBlockCount)}] {percent,3}% {CS_ANIMATION[_animationIndex++ % CS_ANIMATION.Length]}";
  56. UpdateText(text);
  57. ResetTimer();
  58. }
  59. }
  60. private void UpdateText(string text)
  61. {
  62. // Get length of common portion
  63. var commonPrefixLength = 0;
  64. var commonLength = Math.Min(_currentText.Length, text.Length);
  65. while (commonPrefixLength < commonLength && text[commonPrefixLength] == _currentText[commonPrefixLength])
  66. {
  67. commonPrefixLength++;
  68. }
  69. // Backtrack to the first differing character
  70. var outputBuilder = new StringBuilder();
  71. outputBuilder.Append('\b', _currentText.Length - commonPrefixLength);
  72. // Output new suffix
  73. outputBuilder.Append(text.Substring(commonPrefixLength));
  74. // If the new text is shorter than the old one: delete overlapping characters
  75. var overlapCount = _currentText.Length - text.Length;
  76. if (overlapCount > 0)
  77. {
  78. outputBuilder.Append(' ', overlapCount);
  79. outputBuilder.Append('\b', overlapCount);
  80. }
  81. _writer.Write(outputBuilder.ToString());
  82. _currentText = text;
  83. }
  84. private void ResetTimer()
  85. {
  86. _timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1));
  87. }
  88. #endregion
  89. #region *** Private Overrides ***
  90. protected override void OnDisposing()
  91. {
  92. lock (_timer)
  93. {
  94. UpdateText(string.Empty);
  95. _writer = null;
  96. }
  97. }
  98. #endregion
  99. }
  100. }