-
-
Save ngbrown/f33d7e7ca882e9b783836147c5451d14 to your computer and use it in GitHub Desktop.
Console progress bar. Code is under the MIT License: http://opensource.org/licenses/MIT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Threading; | |
static class Program { | |
static void Main() { | |
Console.Write("Performing some task... "); | |
using (var progress = new ProgressBar()) { | |
for (int i = 0; i <= 100; i++) { | |
progress.Report((double) i / 100); | |
Thread.Sleep(20); | |
} | |
} | |
Console.WriteLine("Done."); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Text; | |
using System.Threading; | |
/// <summary> | |
/// An ASCII progress bar | |
/// </summary> | |
/// <remarks> | |
/// Code is under the MIT License | |
/// Based on https://gist.github.com/DanielSWolf/0ab6a96899cc5377bf54 by Daniel Wolf | |
/// </remarks> | |
public class ProgressBar : IDisposable, IProgress<double> | |
{ | |
private const int blockCount = 20; | |
private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8); | |
private const string animation = @"|/-\"; | |
private readonly Timer timer; | |
private double currentProgress = 0; | |
private string currentText = string.Empty; | |
private bool disposed = false; | |
private int animationIndex = 0; | |
public ProgressBar() | |
{ | |
// 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) | |
{ | |
this.disposed = true; | |
} | |
else | |
{ | |
this.timer = new Timer(TimerHandler, default(object), this.animationInterval, this.animationInterval); | |
} | |
} | |
public void Report(double value) | |
{ | |
// Make sure value is in [0..1] range | |
double clipped = Math.Max(0, Math.Min(1, value)); | |
Interlocked.Exchange(ref this.currentProgress, clipped); | |
} | |
private void TimerHandler(object state) | |
{ | |
lock (this.timer) | |
{ | |
if (this.disposed) | |
{ | |
return; | |
} | |
int progressBlockCount = (int) (this.currentProgress * blockCount); | |
int percent = (int) (this.currentProgress * 100); | |
string filled = new string('#', progressBlockCount); | |
string empty = new string(' ', blockCount - progressBlockCount); | |
string text = string.Format("[{0}{1}] {2,3}% {3}", | |
filled, empty, percent, animation[this.animationIndex++ % animation.Length]); | |
UpdateText(text); | |
} | |
} | |
private void UpdateText(string text) | |
{ | |
// Get length of common portion | |
int commonPrefixLength = 0; | |
int commonLength = Math.Min(this.currentText.Length, text.Length); | |
while (commonPrefixLength < commonLength && text[commonPrefixLength] == this.currentText[commonPrefixLength]) | |
{ | |
commonPrefixLength++; | |
} | |
// Backtrack to the first differing character | |
var outputBuilder = new StringBuilder(); | |
outputBuilder.Append('\b', this.currentText.Length - commonPrefixLength); | |
// Output new suffix | |
outputBuilder.Append(text.Substring(commonPrefixLength)); | |
// If the new text is shorter than the old one: delete overlapping characters | |
int overlapCount = this.currentText.Length - text.Length; | |
if (overlapCount > 0) | |
{ | |
outputBuilder.Append(' ', overlapCount); | |
outputBuilder.Append('\b', overlapCount); | |
} | |
Console.Write(outputBuilder); | |
this.currentText = text; | |
} | |
public void Dispose() | |
{ | |
if (this.disposed) | |
{ | |
return; | |
} | |
lock (this.timer) | |
{ | |
this.disposed = true; | |
UpdateText(string.Empty); | |
this.timer.Dispose(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Threading; | |
using System.Threading.Tasks; | |
public class ProgressStream : Stream | |
{ | |
private Stream stream; | |
private readonly IProgress<double> progress; | |
private readonly bool leaveOpen; | |
private long trackedLength; | |
private long trackedPosition; | |
private int? beginCount; | |
public ProgressStream(Stream stream, IProgress<double> progress, long? length = null, bool leaveOpen = false) | |
{ | |
this.stream = stream; | |
this.progress = progress; | |
this.leaveOpen = leaveOpen; | |
this.trackedLength = length ?? stream.Length; | |
if (this.trackedLength == 0) | |
{ | |
throw new InvalidOperationException("Stream must have valid length"); | |
} | |
} | |
public override void CopyTo(Stream destination, int bufferSize) | |
{ | |
this.stream.CopyTo(destination, bufferSize); | |
} | |
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) | |
{ | |
return this.stream.CopyToAsync(destination, bufferSize, cancellationToken); | |
} | |
public override void Close() | |
{ | |
this.Dispose(true); | |
} | |
public override void Flush() | |
{ | |
this.stream.Flush(); | |
} | |
public override Task FlushAsync(CancellationToken cancellationToken) | |
{ | |
return this.stream.FlushAsync(cancellationToken); | |
} | |
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, | |
object state) | |
{ | |
return this.stream.BeginRead(buffer, offset, count, callback, state); | |
} | |
public override int EndRead(IAsyncResult asyncResult) | |
{ | |
int bytesRead = this.stream.EndRead(asyncResult); | |
return AdvanceProgress(bytesRead); | |
} | |
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |
{ | |
var bytesRead = await this.stream.ReadAsync(buffer, offset, count, cancellationToken); | |
return AdvanceProgress(bytesRead); | |
} | |
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = new CancellationToken()) | |
{ | |
var bytesRead = await base.ReadAsync(buffer, cancellationToken); | |
return AdvanceProgress(bytesRead); | |
} | |
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, | |
object state) | |
{ | |
this.beginCount = count; | |
return this.stream.BeginWrite(buffer, offset, count, callback, state); | |
} | |
public override void EndWrite(IAsyncResult asyncResult) | |
{ | |
this.stream.EndWrite(asyncResult); | |
if (asyncResult.IsCompleted && this.beginCount.HasValue) | |
{ | |
AdvanceProgress(this.beginCount.Value); | |
this.beginCount = default; | |
} | |
} | |
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |
{ | |
await this.stream.WriteAsync(buffer, offset, count, cancellationToken); | |
AdvanceProgress(count); | |
} | |
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = new CancellationToken()) | |
{ | |
await base.WriteAsync(buffer, cancellationToken); | |
AdvanceProgress(buffer.Length); | |
} | |
public override long Seek(long offset, SeekOrigin origin) | |
{ | |
var position = this.stream.Seek(offset, origin); | |
switch (origin) | |
{ | |
case SeekOrigin.Begin: | |
this.trackedPosition = position; | |
break; | |
case SeekOrigin.Current: | |
this.trackedPosition += this.trackedLength; | |
break; | |
case SeekOrigin.End: | |
this.trackedPosition = this.trackedLength + offset; | |
break; | |
default: | |
throw new ArgumentOutOfRangeException(nameof(origin), origin, null); | |
} | |
this.ReportProgress(); | |
return position; | |
} | |
public override void SetLength(long value) | |
{ | |
this.stream.SetLength(value); | |
this.trackedLength = value; | |
this.ReportProgress(); | |
} | |
public override int Read(byte[] buffer, int offset, int count) | |
{ | |
int bytesRead = this.stream.Read(buffer, offset, count); | |
return this.AdvanceProgress(bytesRead); | |
} | |
public override int Read(Span<byte> buffer) | |
{ | |
return AdvanceProgress(this.stream.Read(buffer)); | |
} | |
public override int ReadByte() | |
{ | |
int readByte = this.stream.ReadByte(); | |
this.AdvanceProgress(readByte < 0 ? 0 : 1); | |
return readByte; | |
} | |
public override void Write(byte[] buffer, int offset, int count) | |
{ | |
this.stream.Write(buffer, offset, count); | |
} | |
public override void Write(ReadOnlySpan<byte> buffer) | |
{ | |
this.stream.Write(buffer); | |
AdvanceProgress(buffer.Length); | |
} | |
public override void WriteByte(byte value) | |
{ | |
this.stream.WriteByte(value); | |
} | |
public override bool CanRead => this.stream.CanRead; | |
public override bool CanSeek => this.stream.CanSeek; | |
public override bool CanTimeout => this.stream.CanTimeout; | |
public override bool CanWrite => this.stream.CanWrite; | |
public override long Length => this.stream.Length; | |
public override long Position | |
{ | |
get => this.stream.Position; | |
set | |
{ | |
this.stream.Position = value; | |
this.trackedPosition = value; | |
this.ReportProgress(); | |
} | |
} | |
public override int ReadTimeout | |
{ | |
get => this.stream.ReadTimeout; | |
set => this.stream.ReadTimeout = value; | |
} | |
public override int WriteTimeout | |
{ | |
get => this.stream.WriteTimeout; | |
set => this.stream.WriteTimeout = value; | |
} | |
public override async ValueTask DisposeAsync() | |
{ | |
await this.stream.DisposeAsync(); | |
this.stream = null; | |
await base.DisposeAsync(); | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
try | |
{ | |
if (this.leaveOpen || !disposing || this.stream == null) | |
{ | |
return; | |
} | |
this.stream.Close(); | |
} | |
finally | |
{ | |
this.stream = (Stream) null; | |
base.Dispose(disposing); | |
} | |
} | |
private long AdvanceProgress(long bytesRead) | |
{ | |
this.trackedPosition += bytesRead; | |
this.ReportProgress(); | |
return bytesRead; | |
} | |
private int AdvanceProgress(int bytesRead) | |
{ | |
this.trackedPosition += bytesRead; | |
this.ReportProgress(); | |
return bytesRead; | |
} | |
private void ReportProgress() | |
{ | |
this.progress.Report((double) this.trackedPosition / this.trackedLength); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment