diff --git a/examples/Console/Progress/Program.cs b/examples/Console/Progress/Program.cs index 6215b9b..82ae6b0 100644 --- a/examples/Console/Progress/Program.cs +++ b/examples/Console/Progress/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using Spectre.Console; @@ -25,8 +26,12 @@ namespace ProgressExample .Start(ctx => { var random = new Random(DateTime.Now.Millisecond); - var tasks = CreateTasks(ctx, random); + // Create some tasks + var tasks = CreateTasks(ctx, random); + var warpTask = ctx.AddTask("Going to warp", autoStart: false).IsIndeterminate(); + + // Wait for all tasks (except the indeterminate one) to complete while (!ctx.IsFinished) { // Increment progress @@ -44,13 +49,24 @@ namespace ProgressExample // Simulate some delay Thread.Sleep(100); } + + // Now start the "warp" task + warpTask.StartTask(); + warpTask.IsIndeterminate(false); + while (!ctx.IsFinished) + { + warpTask.Increment(12 * random.NextDouble()); + + // Simulate some delay + Thread.Sleep(100); + } }); // Done AnsiConsole.MarkupLine("[green]Done![/]"); } - private static List<(ProgressTask, int)> CreateTasks(ProgressContext progress, Random random) + private static List<(ProgressTask Task, int Delay)> CreateTasks(ProgressContext progress, Random random) { var tasks = new List<(ProgressTask, int)>(); while (tasks.Count < 5) diff --git a/src/Spectre.Console.Tests/Unit/Progress/ProgressColumnFixture.cs b/src/Spectre.Console.Tests/Unit/Progress/ProgressColumnFixture.cs index 9d2550c..58a51bd 100644 --- a/src/Spectre.Console.Tests/Unit/Progress/ProgressColumnFixture.cs +++ b/src/Spectre.Console.Tests/Unit/Progress/ProgressColumnFixture.cs @@ -20,7 +20,7 @@ namespace Spectre.Console.Tests.Unit public string Render() { var console = new FakeConsole(); - var context = new RenderContext(console.Profile.Capabilities); + var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities); console.Write(Column.Render(context, Task, TimeSpan.Zero)); return console.Output; } diff --git a/src/Spectre.Console.Tests/Unit/TextTests.cs b/src/Spectre.Console.Tests/Unit/TextTests.cs index 6504779..013a499 100644 --- a/src/Spectre.Console.Tests/Unit/TextTests.cs +++ b/src/Spectre.Console.Tests/Unit/TextTests.cs @@ -15,7 +15,7 @@ namespace Spectre.Console.Tests.Unit var text = new Text("Foo Bar Baz\nQux\nLol mobile"); // When - var result = ((IRenderable)text).Measure(new RenderContext(caps), 80); + var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80); // Then result.Min.ShouldBe(6); @@ -29,7 +29,7 @@ namespace Spectre.Console.Tests.Unit var text = new Text("Foo Bar Baz\nQux\nLol mobile"); // When - var result = ((IRenderable)text).Measure(new RenderContext(caps), 80); + var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80); // Then result.Max.ShouldBe(11); diff --git a/src/Spectre.Console/Extensions/EnumerableExtensions.cs b/src/Spectre.Console/Extensions/EnumerableExtensions.cs index ef1b7b8..87b754f 100644 --- a/src/Spectre.Console/Extensions/EnumerableExtensions.cs +++ b/src/Spectre.Console/Extensions/EnumerableExtensions.cs @@ -6,6 +6,17 @@ namespace Spectre.Console { internal static class EnumerableExtensions { + public static IEnumerable Repeat(this IEnumerable source, int count) + { + while (count-- > 0) + { + foreach (var item in source) + { + yield return item; + } + } + } + public static int IndexOf(this IEnumerable source, T item) where T : class { diff --git a/src/Spectre.Console/Extensions/Progress/ProgressTaskExtensions.cs b/src/Spectre.Console/Extensions/Progress/ProgressTaskExtensions.cs index a30876b..cf7f5d1 100644 --- a/src/Spectre.Console/Extensions/Progress/ProgressTaskExtensions.cs +++ b/src/Spectre.Console/Extensions/Progress/ProgressTaskExtensions.cs @@ -57,5 +57,22 @@ namespace Spectre.Console task.Value = value; return task; } + + /// + /// Sets whether the task is considered indeterminate or not. + /// + /// The task. + /// Whether the task is considered indeterminate or not. + /// The same instance so that multiple calls can be chained. + public static ProgressTask IsIndeterminate(this ProgressTask task, bool indeterminate = true) + { + if (task is null) + { + throw new ArgumentNullException(nameof(task)); + } + + task.IsIndeterminate = indeterminate; + return task; + } } } diff --git a/src/Spectre.Console/Extensions/RenderableExtensions.cs b/src/Spectre.Console/Extensions/RenderableExtensions.cs index acc2fa5..c7ca2ef 100644 --- a/src/Spectre.Console/Extensions/RenderableExtensions.cs +++ b/src/Spectre.Console/Extensions/RenderableExtensions.cs @@ -27,7 +27,7 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(renderable)); } - var context = new RenderContext(console.Profile.Capabilities); + var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities); var renderables = console.Pipeline.Process(context, new[] { renderable }); return GetSegments(console, context, renderables); diff --git a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs index 226bd54..f3cc883 100644 --- a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs +++ b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs @@ -9,7 +9,7 @@ namespace Spectre.Console.Internal { public string Encode(IAnsiConsole console, IEnumerable renderables) { - var context = new RenderContext(EncoderCapabilities.Default); + var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default); var builder = new StringBuilder(); builder.Append("
\n");
diff --git a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
index 4b3e294..954ead2 100644
--- a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
+++ b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
@@ -20,7 +20,7 @@ namespace Spectre.Console.Internal
     {
         public string Encode(IAnsiConsole console, IEnumerable renderables)
         {
-            var context = new RenderContext(EncoderCapabilities.Default);
+            var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
             var builder = new StringBuilder();
 
             foreach (var renderable in renderables)
diff --git a/src/Spectre.Console/Rendering/RenderContext.cs b/src/Spectre.Console/Rendering/RenderContext.cs
index 88f719e..fd10a79 100644
--- a/src/Spectre.Console/Rendering/RenderContext.cs
+++ b/src/Spectre.Console/Rendering/RenderContext.cs
@@ -9,6 +9,11 @@ namespace Spectre.Console.Rendering
     {
         private readonly IReadOnlyCapabilities _capabilities;
 
+        /// 
+        /// Gets the current color system.
+        /// 
+        public ColorSystem ColorSystem { get; }
+
         /// 
         /// Gets a value indicating whether or not unicode is supported.
         /// 
@@ -28,17 +33,19 @@ namespace Spectre.Console.Rendering
         /// 
         /// Initializes a new instance of the  class.
         /// 
+        /// The color system.
         /// The capabilities.
         /// The justification.
-        public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null)
-            : this(capabilities, justification, false)
+        public RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null)
+            : this(colorSystem, capabilities, justification, false)
         {
         }
 
-        private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
+        private RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
         {
             _capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
 
+            ColorSystem = colorSystem;
             Justification = justification;
             SingleLine = singleLine;
         }
@@ -50,7 +57,7 @@ namespace Spectre.Console.Rendering
         /// A new  instance.
         public RenderContext WithJustification(Justify? justification)
         {
-            return new RenderContext(_capabilities, justification, SingleLine);
+            return new RenderContext(ColorSystem, _capabilities, justification, SingleLine);
         }
 
         /// 
@@ -65,7 +72,7 @@ namespace Spectre.Console.Rendering
         /// A new  instance.
         internal RenderContext WithSingleLine()
         {
-            return new RenderContext(_capabilities, Justification, true);
+            return new RenderContext(ColorSystem, _capabilities, Justification, true);
         }
     }
 }
diff --git a/src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs b/src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs
index 1164325..120a3b4 100644
--- a/src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs
+++ b/src/Spectre.Console/Widgets/Progress/Columns/ProgressBarColumn.cs
@@ -28,6 +28,11 @@ namespace Spectre.Console
         /// 
         public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
 
+        /// 
+        /// Gets or sets the style of an indeterminate progress bar.
+        /// 
+        public Style IndeterminateStyle { get; set; } = ProgressBar.DefaultPulseStyle;
+
         /// 
         public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
         {
@@ -39,6 +44,8 @@ namespace Spectre.Console
                 CompletedStyle = CompletedStyle,
                 FinishedStyle = FinishedStyle,
                 RemainingStyle = RemainingStyle,
+                IndeterminateStyle = IndeterminateStyle,
+                IsIndeterminate = task.IsIndeterminate,
             };
         }
     }
diff --git a/src/Spectre.Console/Widgets/Progress/ProgressContext.cs b/src/Spectre.Console/Widgets/Progress/ProgressContext.cs
index 5faae50..b7dcf41 100644
--- a/src/Spectre.Console/Widgets/Progress/ProgressContext.cs
+++ b/src/Spectre.Console/Widgets/Progress/ProgressContext.cs
@@ -16,9 +16,9 @@ namespace Spectre.Console
         private int _taskId;
 
         /// 
-        /// Gets a value indicating whether or not all tasks have completed.
+        /// Gets a value indicating whether or not all started tasks have completed.
         /// 
-        public bool IsFinished => _tasks.All(task => task.IsFinished);
+        public bool IsFinished => _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
 
         internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
         {
@@ -28,20 +28,41 @@ namespace Spectre.Console
             _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
         }
 
+        /// 
+        /// Adds a task.
+        /// 
+        /// The task description.
+        /// Whether or not the task should start immediately.
+        /// The task's max value.
+        /// The newly created task.
+        public ProgressTask AddTask(string description, bool autoStart = true, double maxValue = 100)
+        {
+            return AddTask(description, new ProgressTaskSettings
+            {
+                AutoStart = autoStart,
+                MaxValue = maxValue,
+            });
+        }
+
         /// 
         /// Adds a task.
         /// 
         /// The task description.
         /// The task settings.
         /// The newly created task.
-        public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null)
+        public ProgressTask AddTask(string description, ProgressTaskSettings settings)
         {
+            if (settings is null)
+            {
+                throw new ArgumentNullException(nameof(settings));
+            }
+
             lock (_taskLock)
             {
-                settings ??= new ProgressTaskSettings();
                 var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
 
                 _tasks.Add(task);
+
                 return task;
             }
         }
diff --git a/src/Spectre.Console/Widgets/Progress/ProgressTask.cs b/src/Spectre.Console/Widgets/Progress/ProgressTask.cs
index 4cbb74a..67afa2c 100644
--- a/src/Spectre.Console/Widgets/Progress/ProgressTask.cs
+++ b/src/Spectre.Console/Widgets/Progress/ProgressTask.cs
@@ -93,6 +93,12 @@ namespace Spectre.Console
         /// 
         public TimeSpan? RemainingTime => GetRemainingTime();
 
+        /// 
+        /// Gets or sets a value indicating whether the ProgressBar shows
+        /// actual values or generic, continuous progress feedback.
+        /// 
+        public bool IsIndeterminate { get; set; }
+
         /// 
         /// Initializes a new instance of the  class.
         /// 
diff --git a/src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs b/src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs
index 3ceb0fb..401d867 100644
--- a/src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs
+++ b/src/Spectre.Console/Widgets/Progress/ProgressTaskSettings.cs
@@ -16,5 +16,10 @@ namespace Spectre.Console
         /// will be auto started. Defaults to true.
         /// 
         public bool AutoStart { get; set; } = true;
+
+        /// 
+        /// Gets the default progress task settings.
+        /// 
+        internal static ProgressTaskSettings Default { get; } = new ProgressTaskSettings();
     }
 }
diff --git a/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs
index c764531..7122fa7 100644
--- a/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs
+++ b/src/Spectre.Console/Widgets/Progress/Renderers/DefaultProgressRenderer.cs
@@ -62,7 +62,7 @@ namespace Spectre.Console
                     _stopwatch.Start();
                 }
 
-                var renderContext = new RenderContext(_console.Profile.Capabilities);
+                var renderContext = new RenderContext(_console.Profile.ColorSystem, _console.Profile.Capabilities);
 
                 var delta = _stopwatch.Elapsed - _lastUpdate;
                 _lastUpdate = _stopwatch.Elapsed;
diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs
index 385f578..045d7d4 100644
--- a/src/Spectre.Console/Widgets/ProgressBar.cs
+++ b/src/Spectre.Console/Widgets/ProgressBar.cs
@@ -1,12 +1,16 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.Linq;
 using Spectre.Console.Rendering;
 
 namespace Spectre.Console
 {
     internal sealed class ProgressBar : Renderable, IHasCulture
     {
+        private const int PULSESIZE = 20;
+        private const int PULSESPEED = 15;
+
         public double Value { get; set; }
         public double MaxValue { get; set; } = 100;
 
@@ -15,11 +19,15 @@ namespace Spectre.Console
         public char UnicodeBar { get; set; } = '━';
         public char AsciiBar { get; set; } = '-';
         public bool ShowValue { get; set; }
+        public bool IsIndeterminate { get; set; }
         public CultureInfo? Culture { get; set; }
 
         public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
         public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
         public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
+        public Style IndeterminateStyle { get; set; } = DefaultPulseStyle;
+
+        internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
 
         protected override Measurement Measure(RenderContext context, int maxWidth)
         {
@@ -30,43 +38,49 @@ namespace Spectre.Console
         protected override IEnumerable Render(RenderContext context, int maxWidth)
         {
             var width = Math.Min(Width ?? maxWidth, maxWidth);
-            var completed = Math.Min(MaxValue, Math.Max(0, Value));
+            var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
+            var isCompleted = completedBarCount >= MaxValue;
 
-            var token = !context.Unicode ? AsciiBar : UnicodeBar;
-            var style = completed >= MaxValue ? FinishedStyle : CompletedStyle;
-
-            var bars = Math.Max(0, (int)(width * (completed / MaxValue)));
-
-            var value = completed.ToString(Culture ?? CultureInfo.InvariantCulture);
-            if (ShowValue)
+            if (IsIndeterminate && !isCompleted)
             {
-                bars = bars - value.Length - 1;
-                bars = Math.Max(0, bars);
+                foreach (var segment in RenderIndeterminate(context, width))
+                {
+                    yield return segment;
+                }
+
+                yield break;
             }
 
-            if (bars < 0)
+            var bar = !context.Unicode ? AsciiBar : UnicodeBar;
+            var style = isCompleted ? FinishedStyle : CompletedStyle;
+            var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
+
+            // Show value?
+            var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
+            if (ShowValue)
+            {
+                barCount = barCount - value.Length - 1;
+                barCount = Math.Max(0, barCount);
+            }
+
+            if (barCount < 0)
             {
                 yield break;
             }
 
-            yield return new Segment(new string(token, bars), style);
+            yield return new Segment(new string(bar, barCount), style);
 
             if (ShowValue)
             {
-                // TODO: Fix this at some point
-                if (bars == 0)
-                {
-                    yield return new Segment(value, style);
-                }
-                else
-                {
-                    yield return new Segment(" " + value, style);
-                }
+                yield return barCount == 0
+                    ? new Segment(value, style)
+                    : new Segment(" " + value, style);
             }
 
-            if (bars < width)
+            // More space available?
+            if (barCount < width)
             {
-                var diff = width - bars;
+                var diff = width - barCount;
                 if (ShowValue)
                 {
                     diff = diff - value.Length - 1;
@@ -76,9 +90,59 @@ namespace Spectre.Console
                     }
                 }
 
-                var remainingToken = ShowRemaining ? token : ' ';
+                var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
+                var remainingToken = ShowRemaining && !legacy ? bar : ' ';
                 yield return new Segment(new string(remainingToken, diff), RemainingStyle);
             }
         }
+
+        private IEnumerable RenderIndeterminate(RenderContext context, int width)
+        {
+            var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
+            var style = IndeterminateStyle ?? DefaultPulseStyle;
+
+            IEnumerable GetPulseSegments()
+            {
+                // For 1-bit and 3-bit colors, fall back to
+                // a simpler versions with only two colors.
+                if (context.ColorSystem == ColorSystem.NoColors ||
+                    context.ColorSystem == ColorSystem.Legacy)
+                {
+                    // First half of the pulse
+                    var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
+
+                    // Second half of the pulse
+                    var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
+                    var bar2 = legacy ? " " : bar;
+                    segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
+
+                    foreach (var segment in segments)
+                    {
+                        yield return segment;
+                    }
+
+                    yield break;
+                }
+
+                for (var index = 0; index < PULSESIZE; index++)
+                {
+                    var position = index / (float)PULSESIZE;
+                    var fade = 0.5f + ((float)Math.Cos(position * Math.PI * 2) / 2.0f);
+                    var color = style.Foreground.Blend(style.Background, fade);
+
+                    yield return new Segment(bar, new Style(foreground: color));
+                }
+            }
+
+            // Get the pulse segments
+            var pulseSegments = GetPulseSegments();
+            pulseSegments = pulseSegments.Repeat((width / PULSESIZE) + 2);
+
+            // Repeat the pulse segments
+            var currentTime = (DateTime.Now - DateTime.Today).TotalSeconds;
+            var offset = (int)(currentTime * PULSESPEED) % PULSESIZE;
+
+            return pulseSegments.Skip(offset).Take(width);
+        }
     }
 }