Skip to content

Instantly share code, notes, and snippets.

@DanielSWolf
Last active December 4, 2024 12:48
Show Gist options
  • Save DanielSWolf/0ab6a96899cc5377bf54 to your computer and use it in GitHub Desktop.
Save DanielSWolf/0ab6a96899cc5377bf54 to your computer and use it in GitHub Desktop.
Console progress bar. Code is under the MIT License: http://opensource.org/licenses/MIT
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.");
}
}
using System;
using System.Text;
using System.Threading;
/// <summary>
/// An ASCII progress bar
/// </summary>
public class ProgressBar : IDisposable, IProgress<double> {
private const int blockCount = 10;
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() {
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();
}
}
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);
}
private void TimerHandler(object state) {
lock (timer) {
if (disposed) return;
int progressBlockCount = (int) (currentProgress * blockCount);
int percent = (int) (currentProgress * 100);
string text = string.Format("[{0}{1}] {2,3}% {3}",
new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount),
percent,
animation[animationIndex++ % animation.Length]);
UpdateText(text);
ResetTimer();
}
}
private void UpdateText(string text) {
// Get length of common portion
int commonPrefixLength = 0;
int commonLength = Math.Min(currentText.Length, text.Length);
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength]) {
commonPrefixLength++;
}
// Backtrack to the first differing character
StringBuilder 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
int overlapCount = currentText.Length - text.Length;
if (overlapCount > 0) {
outputBuilder.Append(' ', overlapCount);
outputBuilder.Append('\b', overlapCount);
}
Console.Write(outputBuilder);
currentText = text;
}
private void ResetTimer() {
timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1));
}
public void Dispose() {
lock (timer) {
disposed = true;
UpdateText(string.Empty);
}
}
}
@ssinfod
Copy link

ssinfod commented Apr 4, 2019

Hello, I have a question.
Since the Timer implements IDisposable, should you not call Dispose() on the timer ?

public void Dispose() {
    lock (timer) {
        disposed = true;
        UpdateText(string.Empty);
        timer.Dispose(); // Something like that...
    }
}

Or it it done by setting disposed = true ?

@petervanleeuwen
Copy link

Does this work with console logging? Or do I lose them...

@DanielSWolf
Copy link
Author

Hello, I have a question.
Since the Timer implements IDisposable, should you not call Dispose() on the timer ?

@ssinfod Sorry for the late reply. Yes, disposing the timer after the lock block would certainly be good style because it would free the timer's resources immediately rather than on garbage collection. On the other hand, it doesn't impact behavior, since the timer fires at most one more time, which is ignored due to the disposed flag which is checked within TimerHandler.

@DanielSWolf
Copy link
Author

@DanielSWolf StackOverflow Question

I've added an answer to your SO question. I hope that explains it.

@DeusLoVults
Copy link

Noice.

@SindiaH
Copy link

SindiaH commented Sep 11, 2019

That's perfect and saving me a lot of time, thank you!!

@americofreitasjr
Copy link

Awesome!

@phsantiago32
Copy link

Awesome, thanks for sharing!

@ahmedwadod
Copy link

Man I loved it. That's so dope!

@omersavas
Copy link

Thanks!

@demlol5
Copy link

demlol5 commented Apr 7, 2020

awesome!

@StavoG
Copy link

StavoG commented Apr 23, 2020

Awesome stuff!!! Thank you

@hepe00
Copy link

hepe00 commented Jun 3, 2020

In production code, it only go to 90% oftenly , how can i fix it.

@hepe00
Copy link

hepe00 commented Jun 3, 2020

Thanks a lot.

@DanielSWolf
Copy link
Author

In production code, it only go to 90% oftenly , how can i fix it.

Check that you actually report a value larger than 0.9. Also, do you dispose the progress bar (either by calling dispose or via using), as shown in the example?

@Epicguru
Copy link

Hi awesome snippet, thanks.
Suggestion: move the StringBuilder on line 62 to be a class variable. No sense in making the garbage collector work overtime when updating text very frequently. Thanks!

@DanielSWolf
Copy link
Author

Suggestion: move the StringBuilder on line 62 to be a class variable. No sense in making the garbage collector work overtime when updating text very frequently.

The code is written so that the text is updated only 8 times per second, regardless of how often a new progress value is reported. This means that no more than 8 StringBuilder instances will be allocated and disposed per second. What's more, C# is pretty clever when it comes to optimization, so chances are the StringBuilder is created on the stack rather than the heap. So I tend to follow the first rule of optimization and favor clarity.

@AlexisDanielCote
Copy link

Thank you very much, your code has saved me

@Daniel-hnr
Copy link

it's awsome💥

@abjdev
Copy link

abjdev commented Nov 23, 2021

awesome

@psavva
Copy link

psavva commented Feb 3, 2022

Awesome!

@haseakash
Copy link

Error CS0246: The type or namespace name 'IProgress' could not be found (are you missing a using directive or an assembly reference?)

@elubaini
Copy link

elubaini commented Mar 7, 2022

Great! Thanks.

@mbodm
Copy link

mbodm commented Mar 31, 2022

Hey,

while the examples being rather nice (great job!:thumbsup:), some async/await variation may raise up in your mind, sooner or later. Maybe. 😉

Since i did this recently by my own, maybe some questions about IProgress<> show up for you too. Maybe something like:

What if my progressbar Console.Write(), inside the IProgress<> handler, is executed after the main() ones,
or even after main() has already finished (in some async/await scenario) ?

I struggled a bit with such questions. But Stephen Toub was to the rescue:

So, "if" it ever happens, that you wanna switch to some async/await version of your code (never say never… 😉), these links may safe you some sanity. At least to me, they were rather helpful. So i thought „maybe post them here“, if someone else lands here (like me), cause of some same related questions or Google queries.

@B-CCCPcekca-HET
Copy link

Thanks for you code!!

Copy link

ghost commented May 6, 2022

@mbodm what do you need async for? My guess is that you subscribe the Report method to some event, like DownloadProgressChanged in WebClient, so it is not blocking any other actions, right?

@mbodm
Copy link

mbodm commented May 7, 2022

@Leendert-JanFloor In a typical modern approach, you are using the TAP pattern. You are also using the HttpClient class more often today, than the WebClient class (but this is not the main topic here).

One of the advantages in a TAP pattern approach is: The user of your library can decide on his own, how and where the progress-handling happens. Example given:

  • Is it spooled onto the UI thread ?
  • Is it spooled onto a threadpool thread ?
  • Etc.

This solves some downsides, your mentioned older callback approach has. Your mentioned approach is called the EAP pattern. Just use your Google Fu techniques, to understand the differences and why EAP is used in the earlier years and TAP is more widely used today.

Besides the differences, the TAP pattern approach is developed together with the uprising of async/await and fits very well into it. Or better said: The TAP pattern approach exists specifically cause of async/await driven scenarios. Which are not that rare today.

Good sources, for all of above stuff, are posts containing 1 of these 2 names:

  • Stephen Toub
  • Stephen Cleary

These 2 guys knew exactly what they are talking about and offer great resources on that topic. The former one is an oustanding developer at MS and has developed most of the .NET threading and async/await stuff. The latter one has a lot of experience in asynchronous .NET programming and has some great resources.

All that said:

The use of IProgress<> (and it´s Report() method) allows your TAP library users, to decide on their own, if the report "callback" is happening on the UI thread or not. Often, in WinForms or WPF scenarios, you want that. Because you can access Form controls (like Button, CheckBox, ProgressBar and so on) solely from the UI thread.

So, if you wanna access your Button directly in the "callback" report, that report has to run on the UI thread. But often you also not want that. Or you use some approach that risks to block your UI thread and you have to handle with a lot of stuff, to not block the UI thread. There is this good old problem of "i can´t move my application window while (in example) my download progress happens!". The TAP pattern and IProgress<> offers solutions for exactly that behaviours.

More on that topic just no longer fits this GitHub issue post. 😉 But at least above statements hopefully give you some starting point. I hope it helped at least a bit, to shed some light on that topic.

Have a nice day!

@MrAlbino
Copy link

This helped me a lot, thanks for sharing. Cheers.

@Mariapori
Copy link

Wow, this is so nice!

@Colourclash
Copy link

This was much appreciated! Thank you for sharing. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment