├── doc ├── pbar-osx.gif ├── children-osx.gif ├── pbar-windows.gif ├── styling-osx.gif ├── styling-windows.gif ├── bar-on-bottom-osx.gif ├── update-on-tick-osx.gif ├── children-no-collapse-osx.gif └── children-no-collapse-windows.gif ├── src ├── nuget-icon.png ├── .build │ ├── MSBuild.Community.Tasks.dll │ └── MSBuild.Community.Tasks.targets ├── ShellProgressBar │ ├── ProgressBarHeight.cs │ ├── ConsoleOutLine.cs │ ├── StringExtensions.cs │ ├── IProgressBar.cs │ ├── Progress.cs │ ├── FixedDurationBar.cs │ ├── ShellProgressBar.csproj │ ├── IndeterminateProgressBar.cs │ ├── IndeterminateChildProgressBar.cs │ ├── TaskbarProgress.cs │ ├── ChildProgressBar.cs │ ├── ProgressBarOptions.cs │ ├── ProgressBarBase.cs │ └── ProgressBar.cs ├── ShellProgressBar.Example │ ├── IProgressBarExample.cs │ ├── TestCases │ │ ├── NeverTicksExample.cs │ │ ├── NeverCompletesExample.cs │ │ ├── LongRunningExample.cs │ │ ├── ZeroMaxTicksExample.cs │ │ ├── NegativeMaxTicksExample.cs │ │ ├── TicksOverflowExample.cs │ │ ├── DrawsOnlyOnTickExample.cs │ │ ├── ThreadedTicksOverflowExample.cs │ │ ├── UpdatesMaxTicksExample.cs │ │ ├── NestedProgressBarPerStepProgress.cs │ │ └── DeeplyNestedProgressBarTreeExample.cs │ ├── Examples │ │ ├── DontDisplayInRealTimeExample.cs │ │ ├── ProgressBarOnBottomExample.cs │ │ ├── StylingExample.cs │ │ ├── MessageBeforeAndAfterExample.cs │ │ ├── ChildrenExample.cs │ │ ├── ExampleBase.cs │ │ ├── IndeterminateProgressExample.cs │ │ ├── IntegrationWithIProgressPercentageExample.cs │ │ ├── AlternateFinishedColorExample.cs │ │ ├── IntegrationWithIProgressExample.cs │ │ ├── ChildrenNoCollapseExample.cs │ │ ├── EstimatedDurationExample.cs │ │ ├── FixedDurationExample.cs │ │ ├── IndeterminateChildrenNoCollapse.cs │ │ ├── DownloadPogressExample.cs │ │ └── PersistMessageExample.cs │ ├── ShellProgressBar.Example.csproj │ └── Program.cs └── ShellProgressBar.sln ├── test └── ShellProgressBar.Tests │ ├── UnitTest1.cs │ └── ShellProgressBar.Tests.csproj ├── .editorconfig ├── LICENSE ├── .gitattributes ├── .gitignore └── README.md /doc/pbar-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/pbar-osx.gif -------------------------------------------------------------------------------- /src/nuget-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/src/nuget-icon.png -------------------------------------------------------------------------------- /doc/children-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/children-osx.gif -------------------------------------------------------------------------------- /doc/pbar-windows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/pbar-windows.gif -------------------------------------------------------------------------------- /doc/styling-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/styling-osx.gif -------------------------------------------------------------------------------- /doc/styling-windows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/styling-windows.gif -------------------------------------------------------------------------------- /doc/bar-on-bottom-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/bar-on-bottom-osx.gif -------------------------------------------------------------------------------- /doc/update-on-tick-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/update-on-tick-osx.gif -------------------------------------------------------------------------------- /doc/children-no-collapse-osx.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/children-no-collapse-osx.gif -------------------------------------------------------------------------------- /doc/children-no-collapse-windows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/doc/children-no-collapse-windows.gif -------------------------------------------------------------------------------- /src/.build/MSBuild.Community.Tasks.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mpdreamz/shellprogressbar/HEAD/src/.build/MSBuild.Community.Tasks.dll -------------------------------------------------------------------------------- /src/ShellProgressBar/ProgressBarHeight.cs: -------------------------------------------------------------------------------- 1 | namespace ShellProgressBar 2 | { 3 | public enum ProgressBarHeight 4 | { 5 | Increment, Decrement 6 | } 7 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/IProgressBarExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example 5 | { 6 | public interface IProgressBarExample 7 | { 8 | Task Start(CancellationToken token); 9 | } 10 | } -------------------------------------------------------------------------------- /src/ShellProgressBar/ConsoleOutLine.cs: -------------------------------------------------------------------------------- 1 | namespace ShellProgressBar 2 | { 3 | public struct ConsoleOutLine 4 | { 5 | public bool Error { get; } 6 | public string Line { get; } 7 | 8 | public ConsoleOutLine(string line, bool error = false) 9 | { 10 | Error = error; 11 | Line = line; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/ShellProgressBar.Tests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace ShellProgressBar.Tests 5 | { 6 | public class UnitTest1 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | using var pbar = new ProgressBar(1000, "task"); 12 | pbar.Tick(); 13 | pbar.WriteLine("Asdad"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*.cs] 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | 7 | [*] 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [*.cshtml] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.{fs,fsx}] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | [*.{md,markdown,json,js,csproj,fsproj,targets}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/NeverTicksExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.TestCases 5 | { 6 | public class NeverTicksExample : IProgressBarExample 7 | { 8 | public Task Start(CancellationToken token) 9 | { 10 | var ticks = 10; 11 | using (var pbar = new ProgressBar(ticks, "A console progress bar that never ticks")) 12 | { 13 | } 14 | return Task.FromResult(1); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/ShellProgressBar/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ShellProgressBar 6 | { 7 | internal static class StringExtensions 8 | { 9 | public static string Excerpt(string phrase, int length = 60) 10 | { 11 | if (string.IsNullOrEmpty(phrase) || phrase.Length < length) 12 | return phrase; 13 | return phrase.Substring(0, length - 3) + "..."; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/NeverCompletesExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.TestCases 5 | { 6 | public class NeverCompletesExample : IProgressBarExample 7 | { 8 | public Task Start(CancellationToken token) 9 | { 10 | var ticks = 5; 11 | using (var pbar = new ProgressBar(ticks, "A console progress bar does not complete")) 12 | { 13 | pbar.Tick(); 14 | pbar.Tick(); 15 | pbar.Tick(); 16 | pbar.Tick(); 17 | } 18 | return Task.FromResult(1); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/LongRunningExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.TestCases 6 | { 7 | public class LongRunningExample : IProgressBarExample 8 | { 9 | public Task Start(CancellationToken token) 10 | { 11 | var ticks = 100; 12 | using (var pbar = new ProgressBar(ticks, "my long running operation", ConsoleColor.Green)) 13 | { 14 | for (var i = 0; i < ticks; i++) 15 | { 16 | pbar.Tick("step " + i); 17 | Thread.Sleep(50); 18 | } 19 | } 20 | return Task.FromResult(1); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/ZeroMaxTicksExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.TestCases 6 | { 7 | public class ZeroMaxTicksExample : IProgressBarExample 8 | { 9 | public Task Start(CancellationToken token) 10 | { 11 | var ticks = 0; 12 | using (var pbar = new ProgressBar(ticks, "my operation with zero ticks", ConsoleColor.Cyan)) 13 | { 14 | for (var i = 0; i < ticks; i++) 15 | { 16 | pbar.Tick("step " + i); 17 | Thread.Sleep(50); 18 | } 19 | } 20 | return Task.FromResult(1); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/DontDisplayInRealTimeExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ShellProgressBar.Example.Examples 4 | { 5 | public class DontDisplayInRealTimeExample : ExampleBase 6 | { 7 | protected override Task StartAsync() 8 | { 9 | const int totalTicks = 5; 10 | var options = new ProgressBarOptions 11 | { 12 | DisplayTimeInRealTime = false 13 | }; 14 | using (var pbar = new ProgressBar(totalTicks, "only draw progress on tick", options)) 15 | { 16 | TickToCompletion(pbar, totalTicks, sleep: 1750); 17 | } 18 | 19 | return Task.CompletedTask; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/NegativeMaxTicksExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.TestCases 6 | { 7 | public class NegativeMaxTicksExample : IProgressBarExample 8 | { 9 | public Task Start(CancellationToken token) 10 | { 11 | var ticks = -100; 12 | using (var pbar = new ProgressBar(ticks, "my operation with negative ticks", ConsoleColor.Cyan)) 13 | { 14 | for (var i = 0; i < ticks; i++) 15 | { 16 | pbar.Tick("step " + i); 17 | Thread.Sleep(50); 18 | } 19 | } 20 | return Task.FromResult(1); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/TicksOverflowExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.TestCases 6 | { 7 | public class TicksOverflowExample : IProgressBarExample 8 | { 9 | public Task Start(CancellationToken token) 10 | { 11 | var ticks = 10; 12 | using (var pbar = new ProgressBar(ticks, "My operation that ticks to often", ConsoleColor.Cyan)) 13 | { 14 | for (var i = 0; i < ticks*10; i++) 15 | { 16 | pbar.Tick("too many steps " + i); 17 | Thread.Sleep(50); 18 | } 19 | } 20 | return Task.FromResult(1); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/ProgressBarOnBottomExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.Examples 5 | { 6 | public class ProgressBarOnBottomExample : ExampleBase 7 | { 8 | protected override Task StartAsync() 9 | { 10 | const int totalTicks = 10; 11 | var options = new ProgressBarOptions 12 | { 13 | ProgressCharacter = '─', 14 | ProgressBarOnBottom = true 15 | }; 16 | using var pbar = new ProgressBar(totalTicks, "progress bar is on the bottom now", options); 17 | TickToCompletion(pbar, totalTicks, sleep: 500); 18 | 19 | return Task.CompletedTask; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/DrawsOnlyOnTickExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.TestCases 5 | { 6 | public class DrawsOnlyOnTickExample : IProgressBarExample 7 | { 8 | public Task Start(CancellationToken token) 9 | { 10 | var ticks = 5; 11 | var updateOnTicksOnlyOptions = new ProgressBarOptions {DisplayTimeInRealTime = false}; 12 | using (var pbar = new ProgressBar(ticks, "only update time on ticks", updateOnTicksOnlyOptions)) 13 | { 14 | for (var i = 0; i < ticks; i++) 15 | { 16 | pbar.Tick("only update time on ticks, current: " + i); 17 | Thread.Sleep(1750); 18 | } 19 | } 20 | return Task.FromResult(1); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/ThreadedTicksOverflowExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ShellProgressBar.Example.TestCases 7 | { 8 | public class ThreadedTicksOverflowExample : IProgressBarExample 9 | { 10 | public Task Start(CancellationToken token) 11 | { 12 | var ticks = 200; 13 | using (var pbar = new ProgressBar(ticks/10, "My operation that ticks to often using threads", ConsoleColor.Cyan)) 14 | { 15 | var threads = Enumerable.Range(0, ticks).Select(i => new Thread(() => pbar.Tick("threaded tick " + i))).ToList(); 16 | foreach (var thread in threads) thread.Start(); 17 | foreach (var thread in threads) thread.Join(); 18 | } 19 | return Task.FromResult(1); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/UpdatesMaxTicksExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.TestCases 6 | { 7 | public class UpdatesMaxTicksExample : IProgressBarExample 8 | { 9 | public Task Start(CancellationToken token) 10 | { 11 | var ticks = 10; 12 | using (var pbar = new ProgressBar(ticks, "My operation that updates maxTicks", ConsoleColor.Cyan)) 13 | { 14 | var sleep = 1000; 15 | for (var i = 0; i < ticks; i++) 16 | { 17 | pbar.Tick("Updating maximum ticks " + i); 18 | if (i == 5) 19 | { 20 | ticks = 120; 21 | pbar.MaxTicks = ticks; 22 | sleep = 50; 23 | } 24 | Thread.Sleep(sleep); 25 | } 26 | } 27 | return Task.FromResult(1); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/ShellProgressBar.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | shellprogressbar-example 7 | ShellProgressBar.Example 8 | $(CurrentVersion) 9 | $(CurrentVersion) 10 | $(CurrentAssemblyVersion) 11 | $(CurrentAssemblyFileVersion) 12 | False 13 | latest 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/StylingExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.Examples 5 | { 6 | public class StylingExample : ExampleBase 7 | { 8 | protected override Task StartAsync() 9 | { 10 | const int totalTicks = 10; 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | ForegroundColorDone = ConsoleColor.DarkGreen, 15 | BackgroundColor = ConsoleColor.DarkGray, 16 | BackgroundCharacter = '\u2593' 17 | }; 18 | using var pbar = new ProgressBar(totalTicks, "showing off styling", options); 19 | TickToCompletion(pbar, totalTicks, sleep: 500, i => 20 | { 21 | if (i > 5) pbar.ForegroundColor = ConsoleColor.Red; 22 | }); 23 | 24 | return Task.CompletedTask; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/MessageBeforeAndAfterExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.Examples 5 | { 6 | public class MessageBeforeAndAfterExample : ExampleBase 7 | { 8 | protected override Task StartAsync() 9 | { 10 | Console.WriteLine("This should not be overwritten"); 11 | const int totalTicks = 10; 12 | var options = new ProgressBarOptions 13 | { 14 | ForegroundColor = ConsoleColor.Yellow, 15 | ForegroundColorDone = ConsoleColor.DarkGreen, 16 | BackgroundColor = ConsoleColor.DarkGray, 17 | BackgroundCharacter = '\u2593' 18 | }; 19 | using (var pbar = new ProgressBar(totalTicks, "showing off styling", options)) 20 | { 21 | TickToCompletion(pbar, totalTicks, sleep: 500, i => 22 | { 23 | pbar.WriteErrorLine($"This should appear above:{i}"); 24 | }); 25 | } 26 | 27 | Console.WriteLine("This should not be overwritten either afterwards"); 28 | return Task.CompletedTask; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/ChildrenExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.Examples 5 | { 6 | public class ChildrenExample : ExampleBase 7 | { 8 | protected override Task StartAsync() 9 | { 10 | const int totalTicks = 10; 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | BackgroundColor = ConsoleColor.DarkGray, 15 | ProgressCharacter = '─' 16 | }; 17 | var childOptions = new ProgressBarOptions 18 | { 19 | ForegroundColor = ConsoleColor.Green, 20 | BackgroundColor = ConsoleColor.DarkGray, 21 | ProgressCharacter = '─' 22 | }; 23 | using var pbar = new ProgressBar(totalTicks, "main progressbar", options); 24 | TickToCompletion(pbar, totalTicks, sleep: 10, childAction: i => 25 | { 26 | using var child = pbar.Spawn(totalTicks, "child actions", childOptions); 27 | TickToCompletion(child, totalTicks, sleep: 100); 28 | }); 29 | return Task.CompletedTask; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ShellProgressBar/IProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ShellProgressBar 4 | { 5 | public interface IProgressBar : IDisposable 6 | { 7 | ChildProgressBar Spawn(int maxTicks, string message, ProgressBarOptions options = null); 8 | 9 | void Tick(string message = null); 10 | void Tick(int newTickCount, string message = null); 11 | 12 | int MaxTicks { get; set; } 13 | string Message { get; set; } 14 | 15 | double Percentage { get; } 16 | int CurrentTick { get; } 17 | 18 | ConsoleColor ForegroundColor { get; set; } 19 | 20 | /// 21 | /// This writes a new line above the progress bar to . 22 | /// Use to update the message inside the progress bar 23 | /// 24 | void WriteLine(string message); 25 | 26 | /// This writes a new line above the progress bar to 27 | void WriteErrorLine(string message); 28 | 29 | IProgress AsProgress(Func message = null, Func percentage = null); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/ExampleBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public abstract class ExampleBase : IProgressBarExample 8 | { 9 | private bool RequestToQuit { get; set; } 10 | 11 | protected void TickToCompletion(IProgressBar pbar, int ticks, int sleep = 1750, Action childAction = null) 12 | { 13 | var initialMessage = pbar.Message; 14 | for (var i = 0; i < ticks && !RequestToQuit; i++) 15 | { 16 | pbar.Message = $"Start {i + 1} of {ticks} {Console.CursorTop}/{Console.WindowHeight}: {initialMessage}"; 17 | childAction?.Invoke(i); 18 | Thread.Sleep(sleep); 19 | pbar.Tick($"End {i + 1} of {ticks} {Console.CursorTop}/{Console.WindowHeight}: {initialMessage}"); 20 | } 21 | } 22 | 23 | public async Task Start(CancellationToken token) 24 | { 25 | RequestToQuit = false; 26 | token.Register(() => RequestToQuit = true); 27 | 28 | await this.StartAsync(); 29 | } 30 | 31 | protected abstract Task StartAsync(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/IndeterminateProgressExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class IndeterminateProgressExample : ExampleBase 8 | { 9 | protected override Task StartAsync() 10 | { 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | ForegroundColorDone = ConsoleColor.DarkGreen, 15 | BackgroundColor = ConsoleColor.DarkGray, 16 | BackgroundCharacter = '\u2593' 17 | }; 18 | 19 | using (var pbar = new IndeterminateProgressBar("Indeterminate", options)) 20 | { 21 | Task.Run( 22 | () => 23 | { 24 | for (var i = 0; i < 1_000; i++) 25 | { 26 | pbar.Message= $"The progress is beating to its own drum (indeterminate) {i}"; 27 | Task.Delay(10).Wait(); 28 | } 29 | }).Wait(); 30 | pbar.Finished(); 31 | pbar.Message= "Finished! Moving on to the next in 5 seconds."; 32 | } 33 | 34 | return Task.CompletedTask; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/IntegrationWithIProgressPercentageExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace ShellProgressBar.Example.Examples 9 | { 10 | public class IntegrationWithIProgressPercentageExample : IProgressBarExample 11 | { 12 | public Task Start(CancellationToken token) 13 | { 14 | using (var pbar = new ProgressBar(100, "A console progress that integrates with IProgress")) 15 | { 16 | ProcessFiles(pbar.AsProgress()); 17 | } 18 | return Task.FromResult(1); 19 | } 20 | 21 | public static void ProcessFiles(IProgress progress) 22 | { 23 | var files = Enumerable.Range(1, 10).Select(e => new FileInfo($"Data{e:D2}.csv")).ToList(); 24 | var i = 0; 25 | foreach (var file in files) 26 | { 27 | DoWork(file); 28 | progress?.Report(++i / (float)files.Count); 29 | } 30 | } 31 | 32 | private static void DoWork(FileInfo file) 33 | { 34 | Thread.Sleep(200); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/AlternateFinishedColorExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class AlternateFinishedColorExample : ExampleBase 8 | { 9 | protected override async Task StartAsync() 10 | { 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | ForegroundColorDone = ConsoleColor.DarkGreen, 15 | ForegroundColorError = ConsoleColor.Red, 16 | BackgroundColor = ConsoleColor.DarkGray, 17 | BackgroundCharacter = '\u2593' 18 | }; 19 | 20 | using var pbar = new ProgressBar(100, "100 ticks", options); 21 | await Task.Run( 22 | () => 23 | { 24 | for (var i = 0; i < 10; i++) 25 | { 26 | Task.Delay(10).Wait(); 27 | pbar.Tick($"Step {i}"); 28 | } 29 | pbar.WriteErrorLine("The task ran into an issue!"); 30 | // OR pbar.ObservedError = true; 31 | }); 32 | pbar.Message= "Indicate the task is done, but the status is not Green."; 33 | } 34 | 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/IntegrationWithIProgressExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace ShellProgressBar.Example.Examples 9 | { 10 | public class IntegrationWithIProgressExample : IProgressBarExample 11 | { 12 | public Task Start(CancellationToken token) 13 | { 14 | var files = Enumerable.Range(1, 10).Select(e => new FileInfo($"Data{e:D2}.csv")).ToList(); 15 | using (var pbar = new ProgressBar(files.Count, "A console progress that integrates with IProgress")) 16 | { 17 | ProcessFiles(files, pbar.AsProgress(e => $"Processed {e.Name}")); 18 | } 19 | return Task.FromResult(1); 20 | } 21 | 22 | public static void ProcessFiles(IEnumerable files, IProgress progress) 23 | { 24 | foreach (var file in files) 25 | { 26 | DoWork(file); 27 | progress?.Report(file); 28 | } 29 | } 30 | 31 | private static void DoWork(FileInfo file) 32 | { 33 | Thread.Sleep(200); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/ChildrenNoCollapseExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ShellProgressBar.Example.Examples 5 | { 6 | public class ChildrenNoCollapseExample : ExampleBase 7 | { 8 | protected override Task StartAsync() 9 | { 10 | const int totalTicks = 10; 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | BackgroundColor = ConsoleColor.DarkYellow, 15 | ProgressCharacter = '─' 16 | }; 17 | var childOptions = new ProgressBarOptions 18 | { 19 | ForegroundColor = ConsoleColor.Green, 20 | BackgroundColor = ConsoleColor.DarkGreen, 21 | ProgressCharacter = '─', 22 | CollapseWhenFinished = false 23 | }; 24 | using var pbar = new ProgressBar(totalTicks, "main progressbar", options); 25 | TickToCompletion(pbar, totalTicks, sleep: 10, childAction: i => 26 | { 27 | using var child = pbar.Spawn(totalTicks, "child actions", childOptions); 28 | TickToCompletion(child, totalTicks, sleep: 100); 29 | }); 30 | 31 | return Task.CompletedTask; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Martijn Laarman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/ShellProgressBar/Progress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ShellProgressBar 4 | { 5 | internal class Progress : IProgress, IDisposable 6 | { 7 | private readonly WeakReference _progressBar; 8 | private readonly Func _message; 9 | private readonly Func _percentage; 10 | 11 | public Progress(IProgressBar progressBar, Func message, Func percentage) 12 | { 13 | _progressBar = new WeakReference(progressBar); 14 | _message = message; 15 | _percentage = percentage ?? (value => value as double? ?? value as float?); 16 | } 17 | 18 | public void Report(T value) 19 | { 20 | if (!_progressBar.TryGetTarget(out var progressBar)) return; 21 | 22 | var message = _message?.Invoke(value); 23 | var percentage = _percentage(value); 24 | if (percentage.HasValue) 25 | progressBar.Tick((int)(percentage * progressBar.MaxTicks), message); 26 | else 27 | progressBar.Tick(message); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | if (_progressBar.TryGetTarget(out var progressBar)) 33 | { 34 | progressBar.Dispose(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/ShellProgressBar.Tests/ShellProgressBar.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/EstimatedDurationExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class EstimatedDurationExample : ExampleBase 8 | { 9 | protected override Task StartAsync() 10 | { 11 | const int totalTicks = 10; 12 | var options = new ProgressBarOptions 13 | { 14 | ProgressCharacter = '─', 15 | ShowEstimatedDuration = true 16 | }; 17 | using (var pbar = new ProgressBar(totalTicks, "you can set the estimated duration too", options)) 18 | { 19 | pbar.EstimatedDuration = TimeSpan.FromMilliseconds(totalTicks * 500); 20 | 21 | var initialMessage = pbar.Message; 22 | for (var i = 0; i < totalTicks; i++) 23 | { 24 | pbar.Message = $"Start {i + 1} of {totalTicks}: {initialMessage}"; 25 | Thread.Sleep(500); 26 | 27 | // Simulate changing estimated durations while progress increases 28 | var estimatedDuration = 29 | TimeSpan.FromMilliseconds(500 * totalTicks) + TimeSpan.FromMilliseconds(300 * i); 30 | pbar.Tick(estimatedDuration, $"End {i + 1} of {totalTicks}: {initialMessage}"); 31 | } 32 | } 33 | 34 | return Task.CompletedTask; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/FixedDurationExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class FixedDurationExample : ExampleBase 8 | { 9 | protected override Task StartAsync() 10 | { 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | ForegroundColorDone = ConsoleColor.DarkGreen, 15 | BackgroundColor = ConsoleColor.DarkGray, 16 | BackgroundCharacter = '\u2593' 17 | }; 18 | var wait = TimeSpan.FromSeconds(5); 19 | using (var pbar = new FixedDurationBar(wait, "", options)) 20 | { 21 | var t = new Thread(()=> LongRunningTask(pbar)); 22 | t.Start(); 23 | 24 | if (!pbar.CompletedHandle.WaitOne(wait)) 25 | Console.Error.WriteLine($"{nameof(FixedDurationBar)} did not signal {nameof(FixedDurationBar.CompletedHandle)} after {wait}"); 26 | 27 | } 28 | 29 | return Task.CompletedTask; 30 | } 31 | 32 | private static void LongRunningTask(FixedDurationBar bar) 33 | { 34 | for (var i = 0; i < 1_000_000; i++) 35 | { 36 | bar.Message = $"{i} events"; 37 | if (bar.IsCompleted) break; 38 | Thread.Sleep(1); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/IndeterminateChildrenNoCollapse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class IndeterminateChildrenNoCollapseExample : ExampleBase 8 | { 9 | protected override async Task StartAsync() 10 | { 11 | const int totalChildren = 2; 12 | Random random = new Random(); 13 | var options = new ProgressBarOptions 14 | { 15 | ForegroundColor = ConsoleColor.Yellow, 16 | BackgroundColor = ConsoleColor.DarkGray, 17 | ProgressCharacter = '─' 18 | }; 19 | var childOptions = new ProgressBarOptions 20 | { 21 | ForegroundColor = ConsoleColor.Green, 22 | BackgroundColor = ConsoleColor.DarkGray, 23 | ProgressCharacter = '─', 24 | CollapseWhenFinished = false 25 | }; 26 | using (var pbar = new ProgressBar(totalChildren, "main progressbar", options)) 27 | { 28 | for (int i = 1; i <= totalChildren; i++) 29 | { 30 | pbar.Message = $"Start {i} of {totalChildren}: main progressbar"; 31 | using (var child = pbar.SpawnIndeterminate("child action " + i, childOptions)) 32 | { 33 | await Task.Delay(1000 * random.Next(5, 15)); 34 | child.Finished(); 35 | } 36 | pbar.Tick(); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ShellProgressBar/FixedDurationBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace ShellProgressBar 5 | { 6 | public class FixedDurationBar : ProgressBar 7 | { 8 | public bool IsCompleted { get; private set; } 9 | 10 | private readonly ManualResetEvent _completedHandle = new ManualResetEvent(false); 11 | public WaitHandle CompletedHandle => _completedHandle; 12 | 13 | public FixedDurationBar(TimeSpan duration, string message, ConsoleColor color) : this(duration, message, new ProgressBarOptions {ForegroundColor = color}) { } 14 | 15 | public FixedDurationBar(TimeSpan duration, string message, ProgressBarOptions options = null) 16 | : base((int)Math.Ceiling(duration.TotalSeconds) * 2, message, options) 17 | { 18 | if (!this.Options.DisplayTimeInRealTime) 19 | throw new ArgumentException( 20 | $"{nameof(ProgressBarOptions)}.{nameof(ProgressBarOptions.DisplayTimeInRealTime)} has to be true for {nameof(FixedDurationBar)}", nameof(options) 21 | ); 22 | } 23 | 24 | private long _seenTicks = 0; 25 | protected override void OnTimerTick() 26 | { 27 | Interlocked.Increment(ref _seenTicks); 28 | this.Tick(); 29 | base.OnTimerTick(); 30 | } 31 | 32 | protected override void OnDone() 33 | { 34 | this.IsCompleted = true; 35 | this._completedHandle.Set(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ShellProgressBar/ShellProgressBar.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ShellProgressBar 5 | ShellProgressBar 6 | netstandard1.3;netstandard2.0;net461 7 | true 8 | 9 | 10 | ShellProgressBar 11 | Cross platform simple and complex progressbars on the command line! 12 | Martijn Laarman 13 | MIT 14 | https://github.com/Mpdreamz/shellprogressbar 15 | https://raw.github.com/Mpdreamz/shellprogressbar/master/src/nuget-icon.png 16 | console;shell;terminal;progress;progressbar 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/ShellProgressBar/IndeterminateProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace ShellProgressBar 5 | { 6 | public class IndeterminateProgressBar : ProgressBar 7 | { 8 | private const int MaxTicksForIndeterminate = 20; 9 | 10 | public IndeterminateProgressBar(string message, ConsoleColor color) : this( 11 | message, 12 | new ProgressBarOptions { ForegroundColor = color }) 13 | { 14 | } 15 | 16 | public IndeterminateProgressBar(string message, ProgressBarOptions options = null) : base( 17 | MaxTicksForIndeterminate, 18 | message, 19 | options) 20 | { 21 | if (options == null) 22 | { 23 | options = new ProgressBarOptions(); 24 | } 25 | 26 | options.DisableBottomPercentage = true; 27 | options.DisplayTimeInRealTime = true; 28 | 29 | if (!this.Options.DisplayTimeInRealTime) 30 | throw new ArgumentException( 31 | $"{nameof(ProgressBarOptions)}.{nameof(ProgressBarOptions.DisplayTimeInRealTime)} has to be true for {nameof(FixedDurationBar)}", 32 | nameof(options) 33 | ); 34 | } 35 | 36 | private long _seenTicks = 0; 37 | 38 | protected override void OnTimerTick() 39 | { 40 | Interlocked.Increment(ref _seenTicks); 41 | if (_seenTicks == MaxTicksForIndeterminate - 1) 42 | { 43 | this.Tick(0); 44 | Interlocked.Exchange(ref _seenTicks, 0); 45 | } 46 | else 47 | { 48 | this.Tick(); 49 | } 50 | 51 | base.OnTimerTick(); 52 | } 53 | 54 | public void Finished() 55 | { 56 | Tick(MaxTicksForIndeterminate); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ShellProgressBar/IndeterminateChildProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | 5 | namespace ShellProgressBar 6 | { 7 | public class IndeterminateChildProgressBar : ChildProgressBar 8 | { 9 | private const int MaxTicksForIndeterminate = 20; 10 | private Timer _timer; 11 | internal IndeterminateChildProgressBar( 12 | string message, 13 | Action scheduleDraw, 14 | Action writeLine, 15 | Action writeError, 16 | ProgressBarOptions options = null, 17 | Action growth = null 18 | ) 19 | : base(MaxTicksForIndeterminate, message, scheduleDraw, writeLine, writeError, options, growth) 20 | { 21 | if (options == null) 22 | { 23 | options = new ProgressBarOptions(); 24 | } 25 | 26 | options.DisableBottomPercentage = true; 27 | _timer = new Timer((s) => OnTimerTick(), null, 500, 500); 28 | } 29 | 30 | private long _seenTicks = 0; 31 | 32 | protected void OnTimerTick() 33 | { 34 | Interlocked.Increment(ref _seenTicks); 35 | if (_seenTicks == MaxTicksForIndeterminate - 1) 36 | { 37 | this.Tick(0); 38 | Interlocked.Exchange(ref _seenTicks, 0); 39 | } 40 | else 41 | { 42 | this.Tick(); 43 | } 44 | DisplayProgress(); 45 | } 46 | 47 | public void Finished() 48 | { 49 | _timer.Change(Timeout.Infinite, Timeout.Infinite); 50 | _timer.Dispose(); 51 | Tick(MaxTicksForIndeterminate); 52 | } 53 | 54 | public void Dispose() 55 | { 56 | if (_timer != null) _timer.Dispose(); 57 | foreach (var c in this.Children) c.Dispose(); 58 | OnDone(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/DownloadPogressExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace ShellProgressBar.Example.Examples 8 | { 9 | public class DownloadProgressExample : ExampleBase 10 | { 11 | protected override Task StartAsync() 12 | { 13 | var files = new string[] 14 | { 15 | "https://github.com/Mpdreamz/shellprogressbar/archive/4.3.0.zip", 16 | "https://github.com/Mpdreamz/shellprogressbar/archive/4.2.0.zip", 17 | "https://github.com/Mpdreamz/shellprogressbar/archive/4.1.0.zip", 18 | "https://github.com/Mpdreamz/shellprogressbar/archive/4.0.0.zip", 19 | }; 20 | var childOptions = new ProgressBarOptions 21 | { 22 | ForegroundColor = ConsoleColor.Yellow, 23 | ProgressCharacter = '\u2593' 24 | }; 25 | using var pbar = new ProgressBar(files.Length, "downloading"); 26 | foreach (var (file, i) in files.Select((f, i) => (f, i))) 27 | { 28 | byte[] data = null; 29 | using var child = pbar.Spawn(100, "page: " + i, childOptions); 30 | try 31 | { 32 | #pragma warning disable CS0618 33 | #pragma warning disable SYSLIB0014 34 | using var client = new WebClient(); 35 | #pragma warning restore CS0618 36 | #pragma warning restore SYSLIB0014 37 | client.DownloadProgressChanged += (o, args) => child.Tick(args.ProgressPercentage); 38 | client.DownloadDataCompleted += (o, args) => data = args.Result; 39 | client.DownloadDataAsync(new Uri(file)); 40 | while (client.IsBusy) 41 | { 42 | Thread.Sleep(1); 43 | } 44 | 45 | pbar.Tick(); 46 | } 47 | catch (WebException error) 48 | { 49 | pbar.WriteLine(error.Message); 50 | } 51 | } 52 | 53 | return Task.CompletedTask; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/NestedProgressBarPerStepProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ShellProgressBar.Example.TestCases 7 | { 8 | public class NestedProgressBarPerStepProgress : IProgressBarExample 9 | { 10 | public Task Start(CancellationToken token) 11 | { 12 | var outerTicks = 10; 13 | using (var pbar = new ProgressBar(outerTicks, "outer progress", ConsoleColor.Cyan)) 14 | { 15 | for (var i = 0; i < outerTicks; i++) 16 | { 17 | InnerProgressBars(pbar); 18 | pbar.Tick(); 19 | } 20 | } 21 | return Task.FromResult(1); 22 | } 23 | 24 | private static void InnerProgressBars(ProgressBar pbar) 25 | { 26 | var innerProgressBars = Enumerable.Range(0, new Random().Next(2, 6)) 27 | .Select(s => pbar.Spawn(new Random().Next(2, 5), $"inner bar {s}")) 28 | .ToList(); 29 | 30 | var maxTicks = innerProgressBars.Max(p => p.MaxTicks); 31 | 32 | for (var ii = 0; ii < maxTicks; ii++) 33 | { 34 | foreach (var p in innerProgressBars) 35 | { 36 | InnerInnerProgressBars(p); 37 | p.Tick(); 38 | } 39 | 40 | 41 | Thread.Sleep(4); 42 | } 43 | foreach (var p in innerProgressBars) p.Dispose(); 44 | } 45 | 46 | private static void InnerInnerProgressBars(ChildProgressBar pbar) 47 | { 48 | var progressBarOption = new ProgressBarOptions { ForegroundColor = ConsoleColor.Yellow }; 49 | var innerProgressBars = Enumerable.Range(0, new Random().Next(1, 3)) 50 | .Select(s => pbar.Spawn(new Random().Next(5, 10), $"inner bar {s}", progressBarOption)) 51 | .ToList(); 52 | if (!innerProgressBars.Any()) return; 53 | 54 | var maxTicks = innerProgressBars.Max(p => p.MaxTicks); 55 | 56 | for (var ii = 0; ii < maxTicks; ii++) 57 | { 58 | foreach (var p in innerProgressBars) 59 | p.Tick(); 60 | } 61 | foreach (var p in innerProgressBars) p.Dispose(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Examples/PersistMessageExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ShellProgressBar.Example.Examples 6 | { 7 | public class PersistMessageExample : ExampleBase 8 | { 9 | protected override Task StartAsync() 10 | { 11 | var options = new ProgressBarOptions 12 | { 13 | ForegroundColor = ConsoleColor.Yellow, 14 | ForegroundColorDone = ConsoleColor.DarkGreen, 15 | ForegroundColorError = ConsoleColor.Red, 16 | BackgroundColor = ConsoleColor.DarkGray, 17 | BackgroundCharacter = '\u2593', 18 | WriteQueuedMessage = o => 19 | { 20 | var writer = o.Error ? Console.Error : Console.Out; 21 | var c = o.Error ? ConsoleColor.DarkRed : ConsoleColor.Blue; 22 | if (o.Line.StartsWith("Report 500")) 23 | { 24 | Console.ForegroundColor = ConsoleColor.Yellow; 25 | writer.WriteLine("Add an extra message, because why not"); 26 | 27 | Console.ForegroundColor = c; 28 | writer.WriteLine(o.Line); 29 | return 2; //signal to the progressbar we wrote two messages 30 | } 31 | Console.ForegroundColor = c; 32 | writer.WriteLine(o.Line); 33 | return 1; 34 | } 35 | }; 36 | var wait = TimeSpan.FromSeconds(6); 37 | using var pbar = new FixedDurationBar(wait, "", options); 38 | var t = new Thread(()=> LongRunningTask(pbar)); 39 | t.Start(); 40 | 41 | if (!pbar.CompletedHandle.WaitOne(wait.Subtract(TimeSpan.FromSeconds(.5)))) 42 | { 43 | pbar.WriteErrorLine($"{nameof(FixedDurationBar)} did not signal {nameof(FixedDurationBar.CompletedHandle)} after {wait}"); 44 | pbar.Dispose(); 45 | } 46 | return Task.CompletedTask; 47 | } 48 | 49 | private static void LongRunningTask(FixedDurationBar bar) 50 | { 51 | for (var i = 0; i < 1_000_000; i++) 52 | { 53 | bar.Message = $"{i} events"; 54 | if (bar.IsCompleted || bar.ObservedError) break; 55 | if (i % 500 == 0) bar.WriteLine($"Report {i} to console above the progressbar"); 56 | Thread.Sleep(1); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ShellProgressBar/TaskbarProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ShellProgressBar 5 | { 6 | public static class TaskbarProgress 7 | { 8 | public enum TaskbarStates 9 | { 10 | NoProgress = 0, 11 | Indeterminate = 0x1, 12 | Normal = 0x2, 13 | Error = 0x4, 14 | Paused = 0x8 15 | } 16 | 17 | [ComImport()] 18 | [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] 19 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 20 | private interface ITaskbarList3 21 | { 22 | // ITaskbarList 23 | [PreserveSig] 24 | void HrInit(); 25 | 26 | [PreserveSig] 27 | void AddTab(IntPtr hwnd); 28 | 29 | [PreserveSig] 30 | void DeleteTab(IntPtr hwnd); 31 | 32 | [PreserveSig] 33 | void ActivateTab(IntPtr hwnd); 34 | 35 | [PreserveSig] 36 | void SetActiveAlt(IntPtr hwnd); 37 | 38 | // ITaskbarList2 39 | [PreserveSig] 40 | void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); 41 | 42 | // ITaskbarList3 43 | [PreserveSig] 44 | void SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); 45 | 46 | [PreserveSig] 47 | void SetProgressState(IntPtr hwnd, TaskbarStates state); 48 | } 49 | 50 | [ComImport] 51 | [Guid("56fdf344-fd6d-11d0-958a-006097c9a090")] 52 | [ClassInterface(ClassInterfaceType.None)] 53 | private class TaskbarInstance 54 | {} 55 | 56 | [DllImport("kernel32.dll")] 57 | static extern IntPtr GetConsoleWindow(); 58 | 59 | private static readonly ITaskbarList3 _taskbarInstance = (ITaskbarList3) new TaskbarInstance(); 60 | private static readonly bool _taskbarSupported = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 61 | 62 | public static void SetState(TaskbarStates taskbarState) 63 | { 64 | if (_taskbarSupported) 65 | _taskbarInstance.SetProgressState(GetConsoleWindow(), taskbarState); 66 | } 67 | 68 | public static void SetValue(double progressValue, double progressMax) 69 | { 70 | if (_taskbarSupported) 71 | _taskbarInstance.SetProgressValue(GetConsoleWindow(), (ulong) progressValue, (ulong) progressMax); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ShellProgressBar/ChildProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | 5 | namespace ShellProgressBar 6 | { 7 | public class ChildProgressBar : ProgressBarBase, IProgressBar 8 | { 9 | private readonly Action _scheduleDraw; 10 | private readonly Action _writeLine; 11 | private readonly Action _writeError; 12 | private readonly Action _growth; 13 | 14 | public DateTime StartDate { get; } = DateTime.Now; 15 | 16 | protected override void DisplayProgress() => _scheduleDraw?.Invoke(); 17 | 18 | internal ChildProgressBar( 19 | int maxTicks, 20 | string message, 21 | Action scheduleDraw, 22 | Action writeLine, 23 | Action writeError, 24 | ProgressBarOptions options = null, 25 | Action growth = null 26 | ) 27 | : base(maxTicks, message, options) 28 | { 29 | _scheduleDraw = scheduleDraw; 30 | _writeLine = writeLine; 31 | _writeError = writeError; 32 | _growth = growth; 33 | _growth?.Invoke(ProgressBarHeight.Increment); 34 | } 35 | 36 | protected override void Grow(ProgressBarHeight direction) => _growth?.Invoke(direction); 37 | 38 | private bool _calledDone; 39 | private readonly object _callOnce = new object(); 40 | protected override void OnDone() 41 | { 42 | if (_calledDone) return; 43 | lock(_callOnce) 44 | { 45 | if (_calledDone) return; 46 | 47 | if (this.EndTime == null) 48 | this.EndTime = DateTime.Now; 49 | 50 | if (this.Options.CollapseWhenFinished) 51 | _growth?.Invoke(ProgressBarHeight.Decrement); 52 | 53 | _calledDone = true; 54 | } 55 | } 56 | 57 | public override void WriteLine(string message) => _writeLine(message); 58 | public override void WriteErrorLine(string message) => _writeError(message); 59 | 60 | public void Dispose() 61 | { 62 | foreach (var c in this.Children) c.Dispose(); 63 | OnDone(); 64 | } 65 | 66 | public IProgress AsProgress(Func message = null, Func percentage = null) 67 | { 68 | return new Progress(this, message, percentage); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/TestCases/DeeplyNestedProgressBarTreeExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ShellProgressBar.Example.TestCases 7 | { 8 | public class DeeplyNestedProgressBarTreeExample : IProgressBarExample 9 | { 10 | public Task Start(CancellationToken token) 11 | { 12 | var random = new Random(); 13 | 14 | var numberOfSteps = 7; 15 | 16 | var overProgressOptions = new ProgressBarOptions 17 | { 18 | DenseProgressBar = true, 19 | ProgressCharacter = '─', 20 | BackgroundColor = ConsoleColor.DarkGray, 21 | EnableTaskBarProgress = RuntimeInformation.IsOSPlatform(OSPlatform.Windows), 22 | }; 23 | 24 | using (var pbar = new ProgressBar(numberOfSteps, "overall progress", overProgressOptions)) 25 | { 26 | var stepBarOptions = new ProgressBarOptions 27 | { 28 | DenseProgressBar = true, 29 | ForegroundColor = ConsoleColor.Cyan, 30 | ForegroundColorDone = ConsoleColor.DarkGreen, 31 | ProgressCharacter = '─', 32 | BackgroundColor = ConsoleColor.DarkGray, 33 | CollapseWhenFinished = true, 34 | }; 35 | Parallel.For(0, numberOfSteps, (i) => 36 | { 37 | var workBarOptions = new ProgressBarOptions 38 | { 39 | DenseProgressBar = true, 40 | ForegroundColor = ConsoleColor.Yellow, 41 | ProgressCharacter = '─', 42 | BackgroundColor = ConsoleColor.DarkGray, 43 | }; 44 | var childSteps = random.Next(1, 5); 45 | using (var childProgress = pbar.Spawn(childSteps, $"step {i} progress", stepBarOptions)) 46 | Parallel.For(0, childSteps, (ci) => 47 | { 48 | var childTicks = random.Next(50, 250); 49 | using (var innerChildProgress = childProgress.Spawn(childTicks, $"step {i}::{ci} progress", workBarOptions)) 50 | { 51 | for (var r = 0; r < childTicks; r++) 52 | { 53 | innerChildProgress.Tick(); 54 | Program.BusyWait(50); 55 | } 56 | } 57 | childProgress.Tick(); 58 | }); 59 | 60 | pbar.Tick(); 61 | }); 62 | } 63 | return Task.FromResult(1); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/ShellProgressBar.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.24720.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShellProgressBar", "ShellProgressBar\ShellProgressBar.csproj", "{4767E390-2159-470C-AE49-E75E2DDF80A4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShellProgressBar.Example", "ShellProgressBar.Example\ShellProgressBar.Example.csproj", "{25EEC6B1-4113-41F4-8181-674E4855F40A}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{DEEB298E-2B68-4DDD-9BBA-8E4BFCB2AA30}" 11 | ProjectSection(SolutionItems) = preProject 12 | .build\MSBuild.Community.Tasks.dll = .build\MSBuild.Community.Tasks.dll 13 | .build\MSBuild.Community.Tasks.targets = .build\MSBuild.Community.Tasks.targets 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{314A5D2B-11E8-4BF3-BB64-B4E15399591C}" 17 | ProjectSection(SolutionItems) = preProject 18 | ..\build.bat = ..\build.bat 19 | ..\README.md = ..\README.md 20 | EndProjectSection 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShellProgressBar.Tests", "..\test\ShellProgressBar.Tests\ShellProgressBar.Tests.csproj", "{7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {4767E390-2159-470C-AE49-E75E2DDF80A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {4767E390-2159-470C-AE49-E75E2DDF80A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {4767E390-2159-470C-AE49-E75E2DDF80A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {4767E390-2159-470C-AE49-E75E2DDF80A4}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {25EEC6B1-4113-41F4-8181-674E4855F40A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {25EEC6B1-4113-41F4-8181-674E4855F40A}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {25EEC6B1-4113-41F4-8181-674E4855F40A}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {25EEC6B1-4113-41F4-8181-674E4855F40A}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {7F6B9B22-0375-46C4-ADEB-30F5BF6DB7B2}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | .idea 9 | 10 | # Build results 11 | 12 | [Dd]ebug/ 13 | [Rr]elease/ 14 | x64/ 15 | */build/ 16 | !build 17 | [Bb]in/ 18 | [Oo]bj/ 19 | 20 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 21 | !packages/*/build/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | *_i.c 28 | *_p.c 29 | *.ilk 30 | *.meta 31 | *.obj 32 | *.pch 33 | *.pdb 34 | *.pgc 35 | *.pgd 36 | *.rsp 37 | *.sbr 38 | *.tlb 39 | *.tli 40 | *.tlh 41 | *.tmp 42 | *.tmp_proj 43 | *.log 44 | *.vspscc 45 | *.vssscc 46 | .builds 47 | *.pidb 48 | *.log 49 | *.scc 50 | 51 | # Visual C++ cache files 52 | ipch/ 53 | *.aps 54 | *.ncb 55 | *.opensdf 56 | *.sdf 57 | *.cachefile 58 | 59 | # Visual Studio profiler 60 | *.psess 61 | *.vsp 62 | *.vspx 63 | 64 | # Guidance Automation Toolkit 65 | *.gpState 66 | 67 | # ReSharper is a .NET coding add-in 68 | _ReSharper*/ 69 | *.[Rr]e[Ss]harper 70 | 71 | # TeamCity is a build add-in 72 | _TeamCity* 73 | 74 | # DotCover is a Code Coverage Tool 75 | *.dotCover 76 | 77 | # NCrunch 78 | *.ncrunch* 79 | .*crunch*.local.xml 80 | 81 | # Installshield output folder 82 | [Ee]xpress/ 83 | 84 | # DocProject is a documentation generator add-in 85 | DocProject/buildhelp/ 86 | DocProject/Help/*.HxT 87 | DocProject/Help/*.HxC 88 | DocProject/Help/*.hhc 89 | DocProject/Help/*.hhk 90 | DocProject/Help/*.hhp 91 | DocProject/Help/Html2 92 | DocProject/Help/html 93 | 94 | # Click-Once directory 95 | publish/ 96 | 97 | # Publish Web Output 98 | *.Publish.xml 99 | *.pubxml 100 | 101 | # Windows Azure Build Output 102 | csx 103 | *.build.csdef 104 | 105 | # Windows Store app package directory 106 | AppPackages/ 107 | 108 | # Others 109 | sql/ 110 | *.Cache 111 | ClientBin/ 112 | [Ss]tyle[Cc]op.* 113 | ~$* 114 | *~ 115 | *.dbmdl 116 | *.[Pp]ublish.xml 117 | *.pfx 118 | *.publishsettings 119 | 120 | # RIA/Silverlight projects 121 | Generated_Code/ 122 | 123 | # Backup & report files from converting an old project file to a newer 124 | # Visual Studio version. Backup files are not needed, because we have git ;-) 125 | _UpgradeReport_Files/ 126 | Backup*/ 127 | UpgradeLog*.XML 128 | UpgradeLog*.htm 129 | 130 | # SQL Server files 131 | App_Data/*.mdf 132 | App_Data/*.ldf 133 | 134 | # ========================= 135 | # Windows detritus 136 | # ========================= 137 | 138 | # Windows image file caches 139 | Thumbs.db 140 | ehthumbs.db 141 | 142 | # Folder config file 143 | Desktop.ini 144 | 145 | # Recycle Bin used on file shares 146 | $RECYCLE.BIN/ 147 | 148 | # Mac crap 149 | .DS_Store 150 | 151 | src/packages 152 | build/tmp 153 | build/tmp/* 154 | build/_out/* 155 | !build/keys 156 | !build/tools/* 157 | !build/tools/scriptcs/* 158 | 159 | .vs 160 | -------------------------------------------------------------------------------- /src/ShellProgressBar.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using ShellProgressBar.Example.Examples; 7 | using ShellProgressBar.Example.TestCases; 8 | 9 | namespace ShellProgressBar.Example 10 | { 11 | class Program 12 | { 13 | private static readonly IList TestCases = new List 14 | { 15 | new PersistMessageExample(), 16 | new FixedDurationExample(), 17 | new DeeplyNestedProgressBarTreeExample(), 18 | new NestedProgressBarPerStepProgress(), 19 | new DrawsOnlyOnTickExample(), 20 | new ThreadedTicksOverflowExample(), 21 | new TicksOverflowExample(), 22 | new NegativeMaxTicksExample(), 23 | new ZeroMaxTicksExample(), 24 | new LongRunningExample(), 25 | new NeverCompletesExample(), 26 | new UpdatesMaxTicksExample(), 27 | new NeverTicksExample(), 28 | new EstimatedDurationExample(), 29 | new IndeterminateProgressExample(), 30 | new IndeterminateChildrenNoCollapseExample(), 31 | new AlternateFinishedColorExample() 32 | }; 33 | 34 | private static readonly IList Examples = new List 35 | { 36 | new DontDisplayInRealTimeExample(), 37 | new StylingExample(), 38 | new ProgressBarOnBottomExample(), 39 | new ChildrenExample(), 40 | new ChildrenNoCollapseExample(), 41 | new IntegrationWithIProgressExample(), 42 | new IntegrationWithIProgressPercentageExample(), 43 | new MessageBeforeAndAfterExample(), 44 | new DeeplyNestedProgressBarTreeExample(), 45 | new EstimatedDurationExample(), 46 | new DownloadProgressExample(), 47 | new AlternateFinishedColorExample() 48 | }; 49 | 50 | public static async Task Main(string[] args) 51 | { 52 | var cts = new CancellationTokenSource(); 53 | Console.CancelKeyPress += (s, e) => 54 | { 55 | cts.Cancel(); 56 | }; 57 | 58 | await MainAsync(args, cts.Token); 59 | } 60 | 61 | private static async Task MainAsync(string[] args, CancellationToken token) 62 | { 63 | var command = args.Length > 0 ? args[0] : "test"; 64 | switch (command) 65 | { 66 | case "test": 67 | await RunTestCases(token); 68 | return; 69 | case "example": 70 | var nth = args.Length > 1 ? int.Parse(args[1]) : 0; 71 | await RunExample(nth, token); 72 | return; 73 | default: 74 | await Console.Error.WriteLineAsync($"Unknown command:{command}"); 75 | return; 76 | } 77 | } 78 | 79 | private static async Task RunExample(int nth, CancellationToken token) 80 | { 81 | if (nth > Examples.Count - 1 || nth < 0) 82 | { 83 | await Console.Error.WriteLineAsync($"There are only {Examples.Count} examples, {nth} is not valid"); 84 | } 85 | 86 | var example = Examples[nth]; 87 | 88 | await example.Start(token); 89 | } 90 | 91 | private static async Task RunTestCases(CancellationToken token) 92 | { 93 | var i = 0; 94 | foreach (var example in TestCases) 95 | { 96 | if (i > 0) Console.Clear(); //not necessary but for demo/recording purposes. 97 | await example.Start(token); 98 | i++; 99 | } 100 | Console.Write("Shown all examples!"); 101 | } 102 | 103 | public static void BusyWait(int milliseconds) 104 | { 105 | Thread.Sleep(milliseconds); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ShellProgressBar/ProgressBarOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ShellProgressBar 5 | { 6 | /// 7 | /// Control the behaviour of your progressbar 8 | /// 9 | public class ProgressBarOptions 10 | { 11 | private bool _enableTaskBarProgress; 12 | public static readonly ProgressBarOptions Default = new ProgressBarOptions(); 13 | 14 | public static string ProgressMessageEncodingName { get; set; } 15 | 16 | public string MessageEncodingName 17 | { 18 | get 19 | { 20 | return ProgressMessageEncodingName; 21 | } 22 | set 23 | { 24 | ProgressMessageEncodingName = value; 25 | } 26 | } 27 | 28 | /// The foreground color of the progress bar, message and time 29 | public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Green; 30 | 31 | /// The foreground color the progressbar has reached a 100 percent 32 | public ConsoleColor? ForegroundColorDone { get; set; } 33 | 34 | /// 35 | /// The foreground color the progressbar when it has observed an error 36 | /// If set this takes priority over any other color as soon as an error is observed 37 | /// Use either or to 38 | /// put the progressbar in errored state 39 | /// 40 | public ConsoleColor? ForegroundColorError { get; set; } 41 | 42 | /// The background color of the remainder of the progressbar 43 | public ConsoleColor? BackgroundColor { get; set; } 44 | 45 | /// The character to use to draw the progressbar 46 | public char ProgressCharacter { get; set; } = '\u2588'; 47 | 48 | /// 49 | /// The character to use for the background of the progress defaults to 50 | /// 51 | public char? BackgroundCharacter { get; set; } 52 | 53 | /// 54 | /// When true will redraw the progressbar using a timer, otherwise only update when 55 | /// is called. 56 | /// Defaults to true 57 | /// 58 | public bool DisplayTimeInRealTime { get; set; } = true; 59 | 60 | /// 61 | /// Collapse the progressbar when done, very useful for child progressbars 62 | /// Defaults to true 63 | /// 64 | public bool CollapseWhenFinished { get; set; } = false; 65 | 66 | /// 67 | /// By default the text and time information is displayed at the bottom and the progress bar at the top. 68 | /// This setting swaps their position 69 | /// 70 | public bool ProgressBarOnBottom { get; set; } 71 | 72 | /// 73 | /// Progressbar is written on a single line 74 | /// 75 | public bool DenseProgressBar { get; set; } 76 | 77 | /// 78 | /// Whether to show the estimated time. It can be set when 79 | /// is called or the property 80 | /// is set. 81 | /// 82 | public bool ShowEstimatedDuration { get; set; } 83 | 84 | /// 85 | /// Whether to show the percentage number 86 | /// 87 | public bool DisableBottomPercentage { get; set; } = false; 88 | 89 | /// Set percentage decimal format. By default is {0:N2}. 90 | public string PercentageFormat { get; set; } = "{0:N2}% "; 91 | 92 | /// 93 | /// Use Windows' task bar to display progress. 94 | /// 95 | /// 96 | /// This feature is available on the Windows platform. 97 | /// 98 | public bool EnableTaskBarProgress 99 | { 100 | get => _enableTaskBarProgress; 101 | set 102 | { 103 | if (value && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 104 | throw new NotSupportedException("Task bar progress only works on Windows"); 105 | 106 | _enableTaskBarProgress = value; 107 | } 108 | } 109 | 110 | /// 111 | /// Take ownership of writing a message that is intended to be displayed above the progressbar. 112 | /// The delegate is expected to return the number of messages written to the console as a result of the string argument. 113 | /// Use case: pretty print or change the console colors, the progressbar will reset back 114 | /// 115 | public Func WriteQueuedMessage { get; set; } 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ShellProgressBar/ProgressBarBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Text; 4 | using System.Threading; 5 | 6 | namespace ShellProgressBar 7 | { 8 | public abstract class ProgressBarBase 9 | { 10 | static ProgressBarBase() 11 | { 12 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 13 | } 14 | 15 | protected readonly DateTime _startDate = DateTime.Now; 16 | private int _maxTicks; 17 | private int _currentTick; 18 | private string _message; 19 | private TimeSpan _estimatedDuration; 20 | 21 | protected ProgressBarBase(int maxTicks, string message, ProgressBarOptions options) 22 | { 23 | this._maxTicks = Math.Max(0, maxTicks); 24 | this._message = message; 25 | this.Options = options ?? ProgressBarOptions.Default; 26 | } 27 | 28 | internal ProgressBarOptions Options { get; } 29 | internal ConcurrentBag Children { get; } = new ConcurrentBag(); 30 | 31 | protected abstract void DisplayProgress(); 32 | 33 | protected virtual void Grow(ProgressBarHeight direction) { } 34 | 35 | protected virtual void OnDone() { } 36 | 37 | public DateTime? EndTime { get; protected set; } 38 | 39 | public bool ObservedError { get; set; } 40 | 41 | private ConsoleColor? _dynamicForegroundColor = null; 42 | public ConsoleColor ForegroundColor 43 | { 44 | get 45 | { 46 | var realColor = _dynamicForegroundColor ?? this.Options.ForegroundColor; 47 | if (this.ObservedError && this.Options.ForegroundColorError.HasValue) 48 | return this.Options.ForegroundColorError.Value; 49 | 50 | return EndTime.HasValue 51 | ? this.Options.ForegroundColorDone ?? realColor 52 | : realColor; 53 | } 54 | set => _dynamicForegroundColor = value; 55 | } 56 | 57 | public int CurrentTick => _currentTick; 58 | 59 | public int MaxTicks 60 | { 61 | get => _maxTicks; 62 | set 63 | { 64 | Interlocked.Exchange(ref _maxTicks, value); 65 | DisplayProgress(); 66 | } 67 | } 68 | 69 | public string Message 70 | { 71 | get => _message; 72 | set 73 | { 74 | Interlocked.Exchange(ref _message, value); 75 | DisplayProgress(); 76 | } 77 | } 78 | 79 | public TimeSpan EstimatedDuration 80 | { 81 | get => _estimatedDuration; 82 | set 83 | { 84 | _estimatedDuration = value; 85 | DisplayProgress(); 86 | } 87 | } 88 | 89 | public double Percentage 90 | { 91 | get 92 | { 93 | var percentage = Math.Max(0, Math.Min(100, (100.0 / this._maxTicks) * this._currentTick)); 94 | // Gracefully handle if the percentage is NaN due to division by 0 95 | if (double.IsNaN(percentage) || percentage < 0) percentage = 100; 96 | return percentage; 97 | } 98 | } 99 | 100 | public bool Collapse => this.EndTime.HasValue && this.Options.CollapseWhenFinished; 101 | 102 | public ChildProgressBar Spawn(int maxTicks, string message, ProgressBarOptions options = null) 103 | { 104 | // if this bar collapses all child progressbar will collapse 105 | if (options?.CollapseWhenFinished == false && this.Options.CollapseWhenFinished) 106 | options.CollapseWhenFinished = true; 107 | 108 | var pbar = new ChildProgressBar( 109 | maxTicks, message, DisplayProgress, WriteLine, WriteErrorLine, options ?? this.Options, d => this.Grow(d) 110 | ); 111 | this.Children.Add(pbar); 112 | DisplayProgress(); 113 | return pbar; 114 | } 115 | 116 | public IndeterminateChildProgressBar SpawnIndeterminate(string message, ProgressBarOptions options = null) 117 | { 118 | // if this bar collapses all child progressbar will collapse 119 | if (options?.CollapseWhenFinished == false && this.Options.CollapseWhenFinished) 120 | options.CollapseWhenFinished = true; 121 | 122 | var pbar = new IndeterminateChildProgressBar( 123 | message, DisplayProgress, WriteLine, WriteErrorLine, options ?? this.Options, d => this.Grow(d) 124 | ); 125 | this.Children.Add(pbar); 126 | DisplayProgress(); 127 | return pbar; 128 | } 129 | 130 | public abstract void WriteLine(string message); 131 | public abstract void WriteErrorLine(string message); 132 | 133 | 134 | public void Tick(string message = null) 135 | { 136 | Interlocked.Increment(ref _currentTick); 137 | FinishTick(message); 138 | } 139 | 140 | public void Tick(int newTickCount, string message = null) 141 | { 142 | Interlocked.Exchange(ref _currentTick, newTickCount); 143 | FinishTick(message); 144 | } 145 | 146 | public void Tick(TimeSpan estimatedDuration, string message = null) 147 | { 148 | Interlocked.Increment(ref _currentTick); 149 | _estimatedDuration = estimatedDuration; 150 | 151 | FinishTick(message); 152 | } 153 | public void Tick(int newTickCount, TimeSpan estimatedDuration, string message = null) 154 | { 155 | Interlocked.Exchange(ref _currentTick, newTickCount); 156 | _estimatedDuration = estimatedDuration; 157 | 158 | FinishTick(message); 159 | } 160 | 161 | private void FinishTick(string message) 162 | { 163 | if (message != null) 164 | Interlocked.Exchange(ref _message, message); 165 | 166 | if (_currentTick >= _maxTicks) 167 | { 168 | this.EndTime = DateTime.Now; 169 | this.OnDone(); 170 | } 171 | DisplayProgress(); 172 | } 173 | 174 | protected static string GetDurationString(TimeSpan duration) 175 | { 176 | if (duration.Days > 0) 177 | { 178 | return $"{duration.Days}D {duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}"; 179 | } 180 | return $"{duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}"; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShellProgressBar 2 | =================== 3 | visualize (concurrent) progress in your console application 4 | 5 | This is a great little library to visualize long running command line tasks. 6 | 7 | .NET Core ready! 8 | 9 | It also supports spawning child progress bars which allows you to visualize dependencies and concurrency rather nicely. 10 | 11 | Tested on OSX 12 | 13 | ![example osx](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/pbar-osx.gif) 14 | 15 | and Windows 16 | 17 | ![example win cmd](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/pbar-windows.gif) 18 | 19 | (Powershell works too, see example further down) 20 | 21 | # Install 22 | 23 | Get it on nuget: http://www.nuget.org/packages/ShellProgressBar/ 24 | 25 | # Usage 26 | 27 | Usage is really straightforward 28 | 29 | ```csharp 30 | const int totalTicks = 10; 31 | var options = new ProgressBarOptions 32 | { 33 | ProgressCharacter = '─', 34 | ProgressBarOnBottom = true 35 | }; 36 | using (var pbar = new ProgressBar(totalTicks, "Initial message", options)) 37 | { 38 | pbar.Tick(); //will advance pbar to 1 out of 10. 39 | //we can also advance and update the progressbar text 40 | pbar.Tick("Step 2 of 10"); 41 | } 42 | ``` 43 | 44 | ## Reporting progression 45 | 46 | There are two ways to report progression. You can use the `Tick()` function as described above. Alternatively you can report progression through an [`IProgress`](https://docs.microsoft.com/en-us/dotnet/api/system.iprogress-1) instance that you obtain by calling `AsProgress()` on the progress bar object. 47 | 48 | For a simple case where the progress type is a `float` value between 0.0 and 1.0 that represents the completion percentage, use `progressBar.AsProgress()`: 49 | 50 | ```csharp 51 | using ProgressBar progressBar = new ProgressBar(10000, "My Progress Message"); 52 | IProgress progress = progressBar.AsProgress(); 53 | progress.Report(0.25); // Advances the progress bar to 25% 54 | ``` 55 | 56 | See `IntegrationWithIProgressExample.cs` and `IntegrationWithIProgressPercentageExample.cs` in the [src/ShellProgressBar.Example/Examples](src/ShellProgressBar.Example/Examples) directory for full examples. 57 | 58 | ## Options 59 | 60 | ### Progress bar position 61 | 62 | ```csharp 63 | const int totalTicks = 10; 64 | var options = new ProgressBarOptions 65 | { 66 | ProgressCharacter = '─', 67 | ProgressBarOnBottom = true 68 | }; 69 | using (var pbar = new ProgressBar(totalTicks, "progress bar is on the bottom now", options)) 70 | { 71 | TickToCompletion(pbar, totalTicks, sleep: 500); 72 | } 73 | ``` 74 | 75 | By default the progress bar is at the top and the message at the bottom. 76 | This can be flipped around if so desired. 77 | 78 | ![bar_on_bottom](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/bar-on-bottom-osx.gif) 79 | 80 | ### Styling changes 81 | 82 | ```csharp 83 | const int totalTicks = 10; 84 | var options = new ProgressBarOptions 85 | { 86 | ForegroundColor = ConsoleColor.Yellow, 87 | ForegroundColorDone = ConsoleColor.DarkGreen, 88 | BackgroundColor = ConsoleColor.DarkGray, 89 | BackgroundCharacter = '\u2593' 90 | }; 91 | using (var pbar = new ProgressBar(totalTicks, "showing off styling", options)) 92 | { 93 | TickToCompletion(pbar, totalTicks, sleep: 500); 94 | } 95 | ``` 96 | 97 | Many aspects can be styled including foreground color, background (inactive portion) 98 | and changing the color on completion. 99 | 100 | ![styling](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/styling-windows.gif) 101 | 102 | 103 | ### No real time update 104 | 105 | By default a timer will draw the screen every 500ms. You can configure the progressbar 106 | to only be drawn when `.Tick()` is called. 107 | 108 | ```csharp 109 | const int totalTicks = 5; 110 | var options = new ProgressBarOptions 111 | { 112 | DisplayTimeInRealTime = false 113 | }; 114 | using (var pbar = new ProgressBar(totalTicks, "only draw progress on tick", options)) 115 | { 116 | TickToCompletion(pbar, totalTicks, sleep:1750); 117 | } 118 | ``` 119 | 120 | If you look at the time passed you will see it skips `02:00` 121 | 122 | 123 | ![update_on_tick](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/update-on-tick-osx.gif) 124 | 125 | ### Descendant progressbars 126 | 127 | A progressbar can spawn child progress bars and each child can spawn 128 | its own progressbars. Each child can have its own styling options. 129 | 130 | This is great to visualize concurrent running tasks. 131 | 132 | ```csharp 133 | const int totalTicks = 10; 134 | var options = new ProgressBarOptions 135 | { 136 | ForegroundColor = ConsoleColor.Yellow, 137 | BackgroundColor = ConsoleColor.DarkYellow, 138 | ProgressCharacter = '─' 139 | }; 140 | var childOptions = new ProgressBarOptions 141 | { 142 | ForegroundColor = ConsoleColor.Green, 143 | BackgroundColor = ConsoleColor.DarkGreen, 144 | ProgressCharacter = '─' 145 | }; 146 | using (var pbar = new ProgressBar(totalTicks, "main progressbar", options)) 147 | { 148 | TickToCompletion(pbar, totalTicks, sleep: 10, childAction: () => 149 | { 150 | using (var child = pbar.Spawn(totalTicks, "child actions", childOptions)) 151 | { 152 | TickToCompletion(child, totalTicks, sleep: 100); 153 | } 154 | }); 155 | } 156 | ``` 157 | 158 | ![children](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/children-osx.gif) 159 | 160 | By default children will collapse when done, making room for new/concurrent progressbars. 161 | 162 | You can keep them around by specifying `CollapseWhenFinished = false` 163 | 164 | ```csharp 165 | var childOptions = new ProgressBarOptions 166 | { 167 | ForegroundColor = ConsoleColor.Green, 168 | BackgroundColor = ConsoleColor.DarkGreen, 169 | ProgressCharacter = '─', 170 | CollapseWhenFinished = false 171 | }; 172 | ``` 173 | 174 | ![children_no_collapse](https://github.com/Mpdreamz/shellprogressbar/raw/master/doc/children-no-collapse-windows.gif) 175 | 176 | 177 | # FixedDurationBar 178 | 179 | `ProgressBar` is great for visualizing tasks with an unknown runtime. If you have a task that you know takes a fixed amount of time there is also a `FixedDurationBar` subclass. 180 | `FixedDurationBar` will `Tick()` automatically but other then that all the options and usage are the same. Except it relies on the real time update feature so disabling that 181 | will throw. 182 | 183 | `FixedDurationBar` exposes an `IsCompleted` and `CompletedHandle` 184 | 185 | 186 | ### Credits 187 | 188 | The initial implementation was inspired by this article. 189 | http://www.bytechaser.com/en/articles/ckcwh8nsyt/display-progress-bar-in-console-application-in-c.aspx 190 | 191 | And obviously anyone who sends a PR to this repository :+1: 192 | -------------------------------------------------------------------------------- /src/.build/MSBuild.Community.Tasks.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $(MSBuildExtensionsPath)\MSBuildCommunityTasks 7 | $([MSBUILD]::Unescape($(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.dll)) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /src/ShellProgressBar/ProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace ShellProgressBar 11 | { 12 | public class ProgressBar : ProgressBarBase, IProgressBar 13 | { 14 | private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 15 | 16 | private readonly ConsoleColor _originalColor; 17 | private readonly Func _writeMessageToConsole; 18 | private readonly int _originalWindowTop; 19 | private readonly int _originalWindowHeight; 20 | private readonly bool _startedRedirected; 21 | private int _originalCursorTop; 22 | private int _isDisposed; 23 | 24 | private Timer _timer; 25 | private int _visibleDescendants = 0; 26 | private readonly AutoResetEvent _displayProgressEvent; 27 | private readonly Task _displayProgress; 28 | 29 | public ProgressBar(int maxTicks, string message, ConsoleColor color) 30 | : this(maxTicks, message, new ProgressBarOptions {ForegroundColor = color}) 31 | { 32 | } 33 | 34 | public ProgressBar(int maxTicks, string message, ProgressBarOptions options = null) 35 | : base(maxTicks, message, options) 36 | { 37 | 38 | _writeMessageToConsole = this.Options.WriteQueuedMessage ?? DefaultConsoleWrite; 39 | _startedRedirected = Console.IsOutputRedirected; 40 | 41 | try 42 | { 43 | _originalCursorTop = Console.CursorTop; 44 | _originalWindowTop = Console.WindowTop; 45 | _originalWindowHeight = Console.WindowHeight + _originalWindowTop; 46 | _originalColor = Console.ForegroundColor; 47 | } 48 | catch 49 | { 50 | _startedRedirected = true; 51 | } 52 | 53 | if (!_startedRedirected) 54 | Console.CursorVisible = false; 55 | 56 | if (this.Options.EnableTaskBarProgress) 57 | TaskbarProgress.SetState(TaskbarProgress.TaskbarStates.Normal); 58 | 59 | if (this.Options.DisplayTimeInRealTime) 60 | _timer = new Timer((s) => OnTimerTick(), null, 500, 500); 61 | else //draw once 62 | _timer = new Timer((s) => 63 | { 64 | _timer.Dispose(); 65 | DisplayProgress(); 66 | }, null, 0, 1000); 67 | 68 | _displayProgressEvent = new AutoResetEvent(false); 69 | _displayProgress = Task.Run(() => 70 | { 71 | while (_isDisposed == 0) 72 | { 73 | if (!_displayProgressEvent.WaitOne(TimeSpan.FromSeconds(10))) 74 | continue; 75 | if (_isDisposed > 0) return; 76 | try 77 | { 78 | UpdateProgress(); 79 | } 80 | catch 81 | { 82 | //don't want to crash background thread 83 | } 84 | } 85 | }); 86 | } 87 | 88 | protected virtual void OnTimerTick() => DisplayProgress(); 89 | 90 | protected override void Grow(ProgressBarHeight direction) 91 | { 92 | switch (direction) 93 | { 94 | case ProgressBarHeight.Increment: 95 | Interlocked.Increment(ref _visibleDescendants); 96 | break; 97 | case ProgressBarHeight.Decrement: 98 | Interlocked.Decrement(ref _visibleDescendants); 99 | break; 100 | } 101 | } 102 | 103 | private void EnsureMainProgressBarVisible(int extraBars = 0) 104 | { 105 | var pbarHeight = this.Options.DenseProgressBar ? 1 : 2; 106 | var neededPadding = Math.Min(_originalWindowHeight - pbarHeight, (1 + extraBars) * pbarHeight); 107 | var difference = _originalWindowHeight - _originalCursorTop; 108 | var write = difference <= neededPadding ? Math.Max(0, Math.Max(neededPadding, difference)) : 0; 109 | 110 | var written = 0; 111 | for (; written < write; written++) 112 | Console.WriteLine(); 113 | if (written == 0) return; 114 | 115 | Console.CursorTop = _originalWindowHeight - (written); 116 | _originalCursorTop = Console.CursorTop - 1; 117 | } 118 | 119 | private void GrowDrawingAreaBasedOnChildren() => EnsureMainProgressBarVisible(_visibleDescendants); 120 | 121 | private struct Indentation 122 | { 123 | public Indentation(ConsoleColor color, bool lastChild) 124 | { 125 | this.ConsoleColor = color; 126 | this.LastChild = lastChild; 127 | } 128 | 129 | public string Glyph => !LastChild ? "├─" : "└─"; 130 | 131 | public readonly ConsoleColor ConsoleColor; 132 | public readonly bool LastChild; 133 | } 134 | 135 | private static void CondensedProgressBar( 136 | double percentage, 137 | string message, 138 | char progressCharacter, 139 | char? progressBackgroundCharacter, 140 | ConsoleColor? backgroundColor, 141 | Indentation[] indentation, 142 | bool progressBarOnTop) 143 | { 144 | var depth = indentation.Length; 145 | var messageWidth = 30; 146 | var maxCharacterWidth = Console.WindowWidth - (depth * 2) + 2; 147 | var truncatedMessage = StringExtensions.Excerpt(message, messageWidth - 2) + " "; 148 | var width = (Console.WindowWidth - (depth * 2) + 2) - truncatedMessage.Length; 149 | 150 | if (!string.IsNullOrWhiteSpace(ProgressBarOptions.ProgressMessageEncodingName)) 151 | { 152 | width = width + message.Length - System.Text.Encoding.GetEncoding(ProgressBarOptions.ProgressMessageEncodingName).GetBytes(message).Length; 153 | } 154 | 155 | var newWidth = (int) ((width * percentage) / 100d); 156 | var progBar = new string(progressCharacter, newWidth); 157 | DrawBottomHalfPrefix(indentation, depth); 158 | Console.Write(truncatedMessage); 159 | Console.Write(progBar); 160 | if (backgroundColor.HasValue) 161 | { 162 | Console.ForegroundColor = backgroundColor.Value; 163 | Console.Write(new string(progressBackgroundCharacter ?? progressCharacter, width - newWidth)); 164 | } 165 | else Console.Write(new string(' ', width - newWidth)); 166 | 167 | Console.ForegroundColor = indentation[depth - 1].ConsoleColor; 168 | } 169 | 170 | 171 | private static void ProgressBarBottomHalf(double percentage, DateTime startDate, DateTime? endDate, 172 | string message, Indentation[] indentation, bool progressBarOnBottom, bool showEstimatedDuration, 173 | TimeSpan estimatedDuration, bool disableBottomPercentage, string percentageFormat) 174 | { 175 | var depth = indentation.Length; 176 | var maxCharacterWidth = Console.WindowWidth - (depth * 2) + 2; 177 | var duration = ((endDate ?? DateTime.Now) - startDate); 178 | var durationString = GetDurationString(duration); 179 | 180 | if (showEstimatedDuration) 181 | durationString += 182 | $" / {GetDurationString(estimatedDuration)}"; 183 | 184 | var column1Width = Console.WindowWidth - durationString.Length - (depth * 2) + 2; 185 | var column2Width = durationString.Length; 186 | 187 | if (!string.IsNullOrWhiteSpace(ProgressBarOptions.ProgressMessageEncodingName)) 188 | { 189 | column1Width = column1Width + message.Length - System.Text.Encoding.GetEncoding(ProgressBarOptions.ProgressMessageEncodingName).GetBytes(message).Length; 190 | } 191 | 192 | if (progressBarOnBottom) 193 | DrawTopHalfPrefix(indentation, depth); 194 | else 195 | DrawBottomHalfPrefix(indentation, depth); 196 | 197 | var format = $"{{0, -{column1Width}}}{{1,{column2Width}}}"; 198 | var percentageFormatedString = string.Format(percentageFormat, percentage); 199 | var truncatedMessage = StringExtensions.Excerpt(percentageFormatedString + message, column1Width); 200 | 201 | if (disableBottomPercentage) 202 | { 203 | truncatedMessage = StringExtensions.Excerpt(message, column1Width); 204 | } 205 | 206 | var formatted = string.Format(format, truncatedMessage, durationString); 207 | var m = formatted + new string(' ', Math.Max(0, maxCharacterWidth - formatted.Length)); 208 | Console.Write(m); 209 | } 210 | 211 | private static void DrawBottomHalfPrefix(Indentation[] indentation, int depth) 212 | { 213 | for (var i = 1; i < depth; i++) 214 | { 215 | var ind = indentation[i]; 216 | Console.ForegroundColor = indentation[i - 1].ConsoleColor; 217 | if (!ind.LastChild) 218 | Console.Write(i == (depth - 1) ? ind.Glyph : "│ "); 219 | else 220 | Console.Write(i == (depth - 1) ? ind.Glyph : " "); 221 | } 222 | 223 | Console.ForegroundColor = indentation[depth - 1].ConsoleColor; 224 | } 225 | 226 | private static void ProgressBarTopHalf( 227 | double percentage, 228 | char progressCharacter, 229 | char? progressBackgroundCharacter, 230 | ConsoleColor? backgroundColor, 231 | Indentation[] indentation, bool progressBarOnTop) 232 | { 233 | var depth = indentation.Length; 234 | var width = Console.WindowWidth - (depth * 2) + 2; 235 | 236 | if (progressBarOnTop) 237 | DrawBottomHalfPrefix(indentation, depth); 238 | else 239 | DrawTopHalfPrefix(indentation, depth); 240 | 241 | var newWidth = (int) ((width * percentage) / 100d); 242 | var progBar = new string(progressCharacter, newWidth); 243 | Console.Write(progBar); 244 | if (backgroundColor.HasValue) 245 | { 246 | Console.ForegroundColor = backgroundColor.Value; 247 | Console.Write(new string(progressBackgroundCharacter ?? progressCharacter, width - newWidth)); 248 | } 249 | else Console.Write(new string(' ', width - newWidth)); 250 | 251 | Console.ForegroundColor = indentation[depth - 1].ConsoleColor; 252 | } 253 | 254 | private static void DrawTopHalfPrefix(Indentation[] indentation, int depth) 255 | { 256 | for (var i = 1; i < depth; i++) 257 | { 258 | var ind = indentation[i]; 259 | Console.ForegroundColor = indentation[i - 1].ConsoleColor; 260 | if (ind.LastChild && i != (depth - 1)) 261 | Console.Write(" "); 262 | else 263 | Console.Write("│ "); 264 | } 265 | 266 | Console.ForegroundColor = indentation[depth - 1].ConsoleColor; 267 | } 268 | 269 | protected override void DisplayProgress() => _displayProgressEvent.Set(); 270 | 271 | private readonly ConcurrentQueue _stickyMessages = new ConcurrentQueue(); 272 | 273 | public override void WriteLine(string message) 274 | { 275 | _stickyMessages.Enqueue(new ConsoleOutLine(message)); 276 | DisplayProgress(); 277 | } 278 | public override void WriteErrorLine(string message) 279 | { 280 | this.ObservedError = true; 281 | _stickyMessages.Enqueue(new ConsoleOutLine(message, error: true)); 282 | DisplayProgress(); 283 | } 284 | 285 | private void UpdateProgress() 286 | { 287 | var mainPercentage = this.Percentage; 288 | 289 | if (this.Options.EnableTaskBarProgress) 290 | TaskbarProgress.SetValue(mainPercentage, 100); 291 | 292 | // write queued console messages, displayprogress is signaled straight after but 293 | // just in case make sure we never write more then 5 in a display progress tick 294 | for (var i = 0; i < 5 && _stickyMessages.TryDequeue(out var m); i++) 295 | WriteConsoleLine(m); 296 | 297 | if (_startedRedirected) return; 298 | 299 | Console.CursorVisible = false; 300 | Console.ForegroundColor = this.ForegroundColor; 301 | 302 | GrowDrawingAreaBasedOnChildren(); 303 | var cursorTop = _originalCursorTop; 304 | var indentation = new[] {new Indentation(this.ForegroundColor, true)}; 305 | 306 | void TopHalf() 307 | { 308 | ProgressBarTopHalf(mainPercentage, 309 | this.Options.ProgressCharacter, 310 | this.Options.BackgroundCharacter, 311 | this.Options.BackgroundColor, 312 | indentation, 313 | this.Options.ProgressBarOnBottom 314 | ); 315 | } 316 | 317 | if (this.Options.DenseProgressBar) 318 | { 319 | CondensedProgressBar(mainPercentage, 320 | this.Message, 321 | this.Options.ProgressCharacter, 322 | this.Options.BackgroundCharacter, 323 | this.Options.BackgroundColor, 324 | indentation, 325 | this.Options.ProgressBarOnBottom 326 | ); 327 | 328 | } 329 | else if (this.Options.ProgressBarOnBottom) 330 | { 331 | ProgressBarBottomHalf(mainPercentage, this._startDate, null, this.Message, indentation, 332 | this.Options.ProgressBarOnBottom, Options.ShowEstimatedDuration, EstimatedDuration, this.Options.DisableBottomPercentage, 333 | Options.PercentageFormat); 334 | Console.SetCursorPosition(0, ++cursorTop); 335 | TopHalf(); 336 | } 337 | else 338 | { 339 | TopHalf(); 340 | Console.SetCursorPosition(0, ++cursorTop); 341 | ProgressBarBottomHalf(mainPercentage, this._startDate, null, this.Message, indentation, 342 | this.Options.ProgressBarOnBottom, Options.ShowEstimatedDuration, EstimatedDuration, this.Options.DisableBottomPercentage, 343 | Options.PercentageFormat); 344 | } 345 | 346 | DrawChildren(this.Children, indentation, ref cursorTop, Options.PercentageFormat); 347 | 348 | ResetToBottom(ref cursorTop); 349 | 350 | Console.SetCursorPosition(0, _originalCursorTop); 351 | Console.ForegroundColor = _originalColor; 352 | 353 | if (!(mainPercentage >= 100)) return; 354 | _timer?.Dispose(); 355 | _timer = null; 356 | } 357 | 358 | private void WriteConsoleLine(ConsoleOutLine m) 359 | { 360 | var resetString = new string(' ', Console.WindowWidth); 361 | Console.Write(resetString); 362 | Console.Write("\r"); 363 | var foreground = Console.ForegroundColor; 364 | var background = Console.BackgroundColor; 365 | var written = _writeMessageToConsole(m); 366 | Console.ForegroundColor = foreground; 367 | Console.BackgroundColor = background; 368 | _originalCursorTop += written; 369 | } 370 | 371 | private static int DefaultConsoleWrite(ConsoleOutLine line) 372 | { 373 | if (line.Error) Console.Error.WriteLine(line.Line); 374 | else Console.WriteLine(line.Line); 375 | return 1; 376 | } 377 | 378 | private void ResetToBottom(ref int cursorTop) 379 | { 380 | var resetString = new string(' ', Console.WindowWidth); 381 | var windowHeight = _originalWindowHeight; 382 | if (cursorTop >= (windowHeight - 1)) return; 383 | do 384 | { 385 | Console.Write(resetString); 386 | } while (++cursorTop < (windowHeight - 1)); 387 | } 388 | 389 | private static void DrawChildren(IEnumerable children, Indentation[] indentation, 390 | ref int cursorTop, string percentageFormat) 391 | { 392 | var view = children.Where(c => !c.Collapse).Select((c, i) => new {c, i}).ToList(); 393 | if (!view.Any()) return; 394 | 395 | var windowHeight = Console.WindowHeight; 396 | var lastChild = view.Max(t => t.i); 397 | foreach (var tuple in view) 398 | { 399 | //Dont bother drawing children that would fall off the screen 400 | if (cursorTop >= (windowHeight - 2)) 401 | return; 402 | 403 | var child = tuple.c; 404 | var currentIndentation = new Indentation(child.ForegroundColor, tuple.i == lastChild); 405 | var childIndentation = NewIndentation(indentation, currentIndentation); 406 | 407 | var percentage = child.Percentage; 408 | Console.ForegroundColor = child.ForegroundColor; 409 | 410 | void TopHalf() 411 | { 412 | ProgressBarTopHalf(percentage, 413 | child.Options.ProgressCharacter, 414 | child.Options.BackgroundCharacter, 415 | child.Options.BackgroundColor, 416 | childIndentation, 417 | child.Options.ProgressBarOnBottom 418 | ); 419 | } 420 | 421 | Console.SetCursorPosition(0, ++cursorTop); 422 | 423 | if (child.Options.DenseProgressBar) 424 | { 425 | CondensedProgressBar(percentage, 426 | child.Message, 427 | child.Options.ProgressCharacter, 428 | child.Options.BackgroundCharacter, 429 | child.Options.BackgroundColor, 430 | childIndentation, 431 | child.Options.ProgressBarOnBottom 432 | ); 433 | } 434 | else if (child.Options.ProgressBarOnBottom) 435 | { 436 | ProgressBarBottomHalf(percentage, child.StartDate, child.EndTime, child.Message, childIndentation, 437 | child.Options.ProgressBarOnBottom, child.Options.ShowEstimatedDuration, 438 | child.EstimatedDuration, child.Options.DisableBottomPercentage, 439 | percentageFormat); 440 | Console.SetCursorPosition(0, ++cursorTop); 441 | TopHalf(); 442 | } 443 | else 444 | { 445 | TopHalf(); 446 | Console.SetCursorPosition(0, ++cursorTop); 447 | ProgressBarBottomHalf(percentage, child.StartDate, child.EndTime, child.Message, childIndentation, 448 | child.Options.ProgressBarOnBottom, child.Options.ShowEstimatedDuration, 449 | child.EstimatedDuration, child.Options.DisableBottomPercentage, 450 | percentageFormat); 451 | } 452 | 453 | DrawChildren(child.Children, childIndentation, ref cursorTop, percentageFormat); 454 | } 455 | } 456 | 457 | private static Indentation[] NewIndentation(Indentation[] array, Indentation append) 458 | { 459 | var result = new Indentation[array.Length + 1]; 460 | Array.Copy(array, result, array.Length); 461 | result[array.Length] = append; 462 | return result; 463 | } 464 | 465 | public void Dispose() 466 | { 467 | if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) 468 | return; 469 | 470 | _timer?.Dispose(); 471 | _timer = null; 472 | 473 | // make sure background task is stopped before we clean up 474 | _displayProgressEvent.Set(); 475 | _displayProgress.Wait(); 476 | 477 | // update one last time - needed because background task might have 478 | // been already in progress before Dispose was called and it might 479 | // have been running for a very long time due to poor performance 480 | // of System.Console 481 | UpdateProgress(); 482 | 483 | //make sure we pop all pending messages 484 | while (_stickyMessages.TryDequeue(out var m)) 485 | WriteConsoleLine(m); 486 | 487 | if (this.EndTime == null) 488 | this.EndTime = DateTime.Now; 489 | 490 | if (this.Options.EnableTaskBarProgress) 491 | TaskbarProgress.SetState(TaskbarProgress.TaskbarStates.NoProgress); 492 | 493 | try 494 | { 495 | foreach (var c in this.Children) c.Dispose(); 496 | } 497 | catch { } 498 | 499 | try 500 | { 501 | var pbarHeight = this.Options.DenseProgressBar ? 1 : 2; 502 | var openDescendantsPadding = (_visibleDescendants * pbarHeight); 503 | var newCursorTop = Math.Min(_originalWindowHeight, _originalCursorTop + pbarHeight + openDescendantsPadding); 504 | Console.CursorVisible = true; 505 | Console.SetCursorPosition(0, newCursorTop); 506 | } 507 | //This is bad and I should feel bad, but i rather eat pbar exceptions in production then causing false negatives 508 | catch { } 509 | } 510 | 511 | public IProgress AsProgress(Func message = null, Func percentage = null) 512 | { 513 | return new Progress(this, message, percentage); 514 | } 515 | } 516 | } 517 | --------------------------------------------------------------------------------