using System; using System.Collections.Generic; using System.Linq; namespace Spectre.Console { /// /// Represents a progress task. /// public sealed class ProgressTask : IProgress { private readonly List _samples; private readonly object _lock; private double _maxValue; private string _description; private double _value; /// /// Gets the task ID. /// public int Id { get; } /// /// Gets or sets the task description. /// public string Description { get => _description; set => Update(description: value); } /// /// Gets or sets the max value of the task. /// public double MaxValue { get => _maxValue; set => Update(maxValue: value); } /// /// Gets or sets the value of the task. /// public double Value { get => _value; set => Update(value: value); } /// /// Gets the start time of the task. /// public DateTime? StartTime { get; private set; } /// /// Gets the stop time of the task. /// public DateTime? StopTime { get; private set; } /// /// Gets the task state. /// public ProgressTaskState State { get; } /// /// Gets a value indicating whether or not the task has started. /// public bool IsStarted => StartTime != null; /// /// Gets a value indicating whether or not the task has finished. /// public bool IsFinished => StopTime != null || Value >= MaxValue; /// /// Gets the percentage done of the task. /// public double Percentage => GetPercentage(); /// /// Gets the speed measured in steps/second. /// public double? Speed => GetSpeed(); /// /// Gets the elapsed time. /// public TimeSpan? ElapsedTime => GetElapsedTime(); /// /// Gets the remaining time. /// 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. /// /// The task ID. /// The task description. /// The task max value. /// Whether or not the task should start automatically. public ProgressTask(int id, string description, double maxValue, bool autoStart = true) { _samples = new List(); _lock = new object(); _maxValue = maxValue; _value = 0; _description = description?.RemoveNewLines()?.Trim() ?? throw new ArgumentNullException(nameof(description)); if (string.IsNullOrWhiteSpace(_description)) { throw new ArgumentException("Task name cannot be empty", nameof(description)); } Id = id; State = new ProgressTaskState(); StartTime = autoStart ? DateTime.Now : null; } /// /// Starts the task. /// public void StartTask() { lock (_lock) { if (StopTime != null) { throw new InvalidOperationException("Stopped tasks cannot be restarted"); } StartTime = DateTime.Now; StopTime = null; } } /// /// Stops and marks the task as finished. /// public void StopTask() { lock (_lock) { var now = DateTime.Now; StartTime ??= now; StopTime = now; } } /// /// Increments the task's value. /// /// The value to increment with. public void Increment(double value) { Update(increment: value); } private void Update( string? description = null, double? maxValue = null, double? increment = null, double? value = null) { lock (_lock) { var startValue = Value; if (description != null) { description = description?.RemoveNewLines()?.Trim(); if (string.IsNullOrWhiteSpace(description)) { throw new InvalidOperationException("Task name cannot be empty."); } _description = description; } if (maxValue != null) { _maxValue = maxValue.Value; } if (increment != null) { _value += increment.Value; } if (value != null) { _value = value.Value; } // Need to cap the max value? if (_value > _maxValue) { _value = _maxValue; } var timestamp = DateTime.Now; var threshold = timestamp - TimeSpan.FromSeconds(30); // Remove samples that's too old while (_samples.Count > 0 && _samples[0].Timestamp < threshold) { _samples.RemoveAt(0); } // Keep maximum of 1000 samples while (_samples.Count > 1000) { _samples.RemoveAt(0); } _samples.Add(new ProgressSample(timestamp, Value - startValue)); } } private double GetPercentage() { var percentage = (Value / MaxValue) * 100; percentage = Math.Min(100, Math.Max(0, percentage)); return percentage; } private double? GetSpeed() { lock (_lock) { if (StartTime == null) { return null; } if (_samples.Count == 0) { return null; } var totalTime = _samples.Last().Timestamp - _samples[0].Timestamp; if (totalTime == TimeSpan.Zero) { return null; } var totalCompleted = _samples.Sum(x => x.Value); return totalCompleted / totalTime.TotalSeconds; } } private TimeSpan? GetElapsedTime() { lock (_lock) { if (StartTime == null) { return null; } if (StopTime != null) { return StopTime - StartTime; } return DateTime.Now - StartTime; } } private TimeSpan? GetRemainingTime() { lock (_lock) { if (IsFinished) { return TimeSpan.Zero; } var speed = GetSpeed(); if (speed == null || speed == 0) { return null; } // If the speed is near zero, the estimate below causes the // TimeSpan creation to throw an OverflowException. Just return // the maximum possible remaining time instead of overflowing. var estimate = (MaxValue - Value) / speed.Value; if (estimate > TimeSpan.MaxValue.TotalSeconds) { return TimeSpan.MaxValue; } return TimeSpan.FromSeconds(estimate); } } /// void IProgress.Report(double value) { Update(increment: value - Value); } } }