Add support for different spinners

This commit is contained in:
Patrik Svensson 2020-12-05 10:52:51 +01:00 committed by Patrik Svensson
parent 3c504155bc
commit cbed41e637
20 changed files with 3618 additions and 45 deletions

View File

@ -1,26 +1,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Spectre.Console; using Spectre.Console;
namespace ColumnsExample namespace ColumnsExample
{ {
public static class Program public static class Program
{ {
public static async Task Main() public static void Main()
{ {
// Download some random users
using var client = new HttpClient();
dynamic users = JObject.Parse(
await client.GetStringAsync("https://randomuser.me/api/?results=15"));
// Create a card for each user
var cards = new List<Panel>(); var cards = new List<Panel>();
foreach(var user in users.results) foreach(var user in User.LoadUsers())
{ {
cards.Add(new Panel(GetCardContent(user)) cards.Add(
.Header($"{user.location.country}") new Panel(GetCardContent(user))
.Header($"{user.Country}")
.RoundedBorder().Expand()); .RoundedBorder().Expand());
} }
@ -28,12 +20,12 @@ namespace ColumnsExample
AnsiConsole.Render(new Columns(cards)); AnsiConsole.Render(new Columns(cards));
} }
private static string GetCardContent(dynamic user) private static string GetCardContent(User user)
{ {
var name = $"{user.name.first} {user.name.last}"; var name = $"{user.FirstName} {user.LastName}";
var country = $"{user.location.city}"; var city = $"{user.City}";
return $"[b]{name}[/]\n[yellow]{country}[/]"; return $"[b]{name}[/]\n[yellow]{city}[/]";
} }
} }
} }

89
examples/Columns/User.cs Normal file
View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
namespace ColumnsExample
{
public sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public string Country { get; set; }
public static List<User> LoadUsers()
{
return new List<User>
{
new User
{
FirstName = "Andrea",
LastName = "Johansen",
City = "Hornbæk",
Country = "Denmark",
},
new User
{
FirstName = "Brandon",
LastName = "Cole",
City = "Washington",
Country = "United States",
},
new User
{
FirstName = "Patrik",
LastName = "Svensson",
City = "Stockholm",
Country = "Sweden",
},
new User
{
FirstName = "Freya",
LastName = "Thompson",
City = "Rotorua",
Country = "New Zealand",
},
new User
{
FirstName = "طاها",
LastName = "رضایی",
City = "اهواز",
Country = "Iran",
},
new User
{
FirstName = "Yara",
LastName = "Simon",
City = "Develier",
Country = "Switzerland",
},
new User
{
FirstName = "Giray",
LastName = "Erbay",
City = "Karabük",
Country = "Turkey",
},
new User
{
FirstName = "Miodrag",
LastName = "Schaffer",
City = "Möckern",
Country = "Germany",
},
new User
{
FirstName = "Carmela",
LastName = "Lo Castro",
City = "Firenze",
Country = "Italy",
},
new User
{
FirstName = "Roberto",
LastName = "Sims",
City = "Mallow",
Country = "Ireland",
},
};
}
}
}

View File

@ -0,0 +1,22 @@
##########################################################
# Script that generates progress spinners.
##########################################################
$Output = Join-Path $PSScriptRoot "Temp"
$Source = Join-Path $PSScriptRoot "/../../src/Spectre.Console"
if(!(Test-Path $Output -PathType Container)) {
New-Item -ItemType Directory -Path $Output | Out-Null
}
# Generate the files
Push-Location Generator
&dotnet run -- spinners "$Output" --input $Output
if(!$?) {
Pop-Location
Throw "An error occured when generating code."
}
Pop-Location
# Copy the files to the correct location
Copy-Item (Join-Path "$Output" "ProgressSpinner.Generated.cs") -Destination "$Source/Progress/ProgressSpinner.Generated.cs"

View File

@ -7,7 +7,7 @@ using Spectre.IO;
namespace Generator.Commands namespace Generator.Commands
{ {
public sealed class ColorGeneratorCommand : Command<GeneratorCommandSettings> public sealed class ColorGeneratorCommand : Command<ColorGeneratorCommand.Settings>
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -16,7 +16,13 @@ namespace Generator.Commands
_fileSystem = new FileSystem(); _fileSystem = new FileSystem();
} }
public override int Execute(CommandContext context, GeneratorCommandSettings settings) public sealed class Settings : GeneratorSettings
{
[CommandOption("-i|--input <PATH>")]
public string Input { get; set; }
}
public override int Execute(CommandContext context, Settings settings)
{ {
var templates = new FilePath[] var templates = new FilePath[]
{ {
@ -50,13 +56,4 @@ namespace Generator.Commands
return 0; return 0;
} }
} }
public sealed class GeneratorCommandSettings : CommandSettings
{
[CommandArgument(0, "<OUTPUT>")]
public string Output { get; set; }
[CommandOption("-i|--input <PATH>")]
public string Input { get; set; }
}
} }

View File

@ -15,7 +15,7 @@ using SpectreEnvironment = Spectre.IO.Environment;
namespace Generator.Commands namespace Generator.Commands
{ {
public sealed class EmojiGeneratorCommand : AsyncCommand<GeneratorCommandSettings> public sealed class EmojiGeneratorCommand : AsyncCommand<EmojiGeneratorCommand.Settings>
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IEnvironment _environment; private readonly IEnvironment _environment;
@ -24,9 +24,15 @@ namespace Generator.Commands
private readonly Dictionary<string, string> _templates = new Dictionary<string, string> private readonly Dictionary<string, string> _templates = new Dictionary<string, string>
{ {
{ "Templates/Emoji.Generated.template", "Emoji.Generated.cs" }, { "Templates/Emoji.Generated.template", "Emoji.Generated.cs" },
{ "Templates/Emoji.Json.template", "emojis.json" }, { "Templates/Emoji.Json.template", "emojis.json" }, // For documentation
}; };
public sealed class Settings : GeneratorSettings
{
[CommandOption("-i|--input <PATH>")]
public string Input { get; set; }
}
public EmojiGeneratorCommand() public EmojiGeneratorCommand()
{ {
_fileSystem = new FileSystem(); _fileSystem = new FileSystem();
@ -34,7 +40,7 @@ namespace Generator.Commands
_parser = new HtmlParser(); _parser = new HtmlParser();
} }
public override async Task<int> ExecuteAsync(CommandContext context, GeneratorCommandSettings settings) public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
{ {
var output = new DirectoryPath(settings.Output); var output = new DirectoryPath(settings.Output);
if (!_fileSystem.Directory.Exists(settings.Output)) if (!_fileSystem.Directory.Exists(settings.Output))
@ -60,7 +66,7 @@ namespace Generator.Commands
return 0; return 0;
} }
private async Task<Stream> FetchEmojis(GeneratorCommandSettings settings) private async Task<Stream> FetchEmojis(Settings settings)
{ {
var input = string.IsNullOrEmpty(settings.Input) var input = string.IsNullOrEmpty(settings.Input)
? _environment.WorkingDirectory ? _environment.WorkingDirectory

View File

@ -0,0 +1,10 @@
using Spectre.Cli;
namespace Generator.Commands
{
public class GeneratorSettings : CommandSettings
{
[CommandArgument(0, "<OUTPUT>")]
public string Output { get; set; }
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using Generator.Models;
using Scriban;
using Spectre.Cli;
using Spectre.IO;
namespace Generator.Commands
{
public sealed class SpinnerGeneratorCommand : Command<GeneratorSettings>
{
private readonly IFileSystem _fileSystem;
public SpinnerGeneratorCommand()
{
_fileSystem = new FileSystem();
}
public override int Execute(CommandContext context, GeneratorSettings settings)
{
// Read the spinner model.
var spinners = new List<Spinner>();
spinners.AddRange(Spinner.Parse(File.ReadAllText("Data/spinners.json")));
spinners.AddRange(Spinner.Parse(File.ReadAllText("Data/spinners_default.json")));
var output = new DirectoryPath(settings.Output);
if (!_fileSystem.Directory.Exists(settings.Output))
{
_fileSystem.Directory.Create(settings.Output);
}
// Parse the Scriban template.
var templatePath = new FilePath("Templates/ProgressSpinner.Generated.template");
var template = Template.Parse(File.ReadAllText(templatePath.FullPath));
// Render the template with the model.
var result = template.Render(new { Spinners = spinners });
// Write output to file
var file = output.CombineWithFilePath(templatePath.GetFilename().ChangeExtension(".cs"));
File.WriteAllText(file.FullPath, result);
return 0;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
{
"Default": {
"interval": 100,
"unicode": true,
"frames": [
"⣷",
"⣯",
"⣟",
"⡿",
"⢿",
"⣻",
"⣽",
"⣾"
]
},
"Ascii": {
"interval": 100,
"unicode": true,
"frames": [
"-",
"\\",
"|",
"/",
"-",
"\\",
"|",
"/"
]
}
}

View File

@ -15,6 +15,9 @@
<None Update="Data\colors.json"> <None Update="Data\colors.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Data\spinners.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\ColorTable.Generated.template"> <None Update="Templates\ColorTable.Generated.template">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
@ -24,6 +27,9 @@
<None Update="Templates\ColorPalette.Generated.template"> <None Update="Templates\ColorPalette.Generated.template">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Templates\ProgressSpinner.Generated.template">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Templates\Emoji.Json.template"> <None Update="Templates\Emoji.Json.template">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using Newtonsoft.Json;
namespace Generator.Models
{
public sealed class Spinner
{
public string Name { get; set; }
public string NormalizedName { get; set; }
public int Interval { get; set; }
public bool Unicode { get; set; }
public List<string> Frames { get; set; }
public static IEnumerable<Spinner> Parse(string json)
{
var data = JsonConvert.DeserializeObject<Dictionary<string, Spinner>>(json);
foreach (var item in data)
{
item.Value.Name = item.Key;
item.Value.NormalizedName = item.Value.Name.Pascalize();
var frames = item.Value.Frames;
item.Value.Frames = frames.Select(f => f.Replace("\\", "\\\\")).ToList();
}
return data.Values;
}
}
}

View File

@ -12,6 +12,7 @@ namespace Generator
{ {
config.AddCommand<ColorGeneratorCommand>("colors"); config.AddCommand<ColorGeneratorCommand>("colors");
config.AddCommand<EmojiGeneratorCommand>("emoji"); config.AddCommand<EmojiGeneratorCommand>("emoji");
config.AddCommand<SpinnerGeneratorCommand>("spinners");
}); });
return app.Run(args); return app.Run(args);

View File

@ -2,6 +2,7 @@
// <auto-generated> // <auto-generated>
// This code was generated by a tool. // This code was generated by a tool.
// Generated {{ date.now | date.to_string `%F %R` }} // Generated {{ date.now | date.to_string `%F %R` }}
// Generated from https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
// //
// Changes to this file may cause incorrect behavior and will be lost if // Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated. // the code is regenerated.

View File

@ -0,0 +1,45 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Generated {{ date.now | date.to_string `%F %R` }}
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
namespace Spectre.Console
{
public abstract partial class ProgressSpinner
{
{{~ for spinner in spinners ~}}
private sealed class {{ spinner.normalized_name }}Spinner : ProgressSpinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds({{ spinner.interval }});
public override bool IsUnicode => {{ spinner.unicode }};
public override IReadOnlyList<string> Frames => new List<string>
{
{{~ for frame in spinner.frames ~}}
"{{ frame }}",
{{~ end ~}}
};
}
{{~ end ~}}
/// <summary>
/// Contains all predefined spinners.
/// </summary>
public static class Known
{
{{~ for spinner in spinners ~}}
/// <summary>
/// Gets the "{{ spinner.name }}" spinner.
/// </summary>
public static ProgressSpinner {{ spinner.normalized_name }} { get; } = new {{ spinner.normalized_name }}Spinner();
{{~ end ~}}
}
}
}

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
namespace Spectre.Console namespace Spectre.Console
@ -11,8 +13,8 @@ namespace Spectre.Console
private const string ACCUMULATED = "SPINNER_ACCUMULATED"; private const string ACCUMULATED = "SPINNER_ACCUMULATED";
private const string INDEX = "SPINNER_INDEX"; private const string INDEX = "SPINNER_INDEX";
private readonly string _ansiSequence = "⣷⣯⣟⡿⢿⣻⣽⣾"; private readonly ProgressSpinner _spinner;
private readonly string _asciiSequence = "-\\|/-\\|/"; private int? _maxLength;
/// <inheritdoc/> /// <inheritdoc/>
protected internal override int? ColumnWidth => 1; protected internal override int? ColumnWidth => 1;
@ -25,26 +27,48 @@ namespace Spectre.Console
/// </summary> /// </summary>
public Style Style { get; set; } = new Style(foreground: Color.Yellow); public Style Style { get; set; } = new Style(foreground: Color.Yellow);
/// <summary>
/// Initializes a new instance of the <see cref="SpinnerColumn"/> class.
/// </summary>
public SpinnerColumn()
: this(ProgressSpinner.Known.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SpinnerColumn"/> class.
/// </summary>
/// <param name="spinner">The spinner to use.</param>
public SpinnerColumn(ProgressSpinner spinner)
{
_spinner = spinner ?? throw new ArgumentNullException(nameof(spinner));
}
/// <inheritdoc/> /// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime) public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{ {
var useAscii = (context.LegacyConsole || !context.Unicode) && _spinner.IsUnicode;
var spinner = useAscii ? ProgressSpinner.Known.Ascii : _spinner;
if (!task.IsStarted || task.IsFinished) if (!task.IsStarted || task.IsFinished)
{ {
return new Markup(" "); if (_maxLength == null)
{
_maxLength = _spinner.Frames.Max(frame => Cell.GetCellLength(context, frame));
}
return new Markup(new string(' ', _maxLength.Value));
} }
var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds); var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds);
if (accumulated >= 100) if (accumulated >= _spinner.Interval.TotalMilliseconds)
{ {
task.State.Update<double>(ACCUMULATED, _ => 0); task.State.Update<double>(ACCUMULATED, _ => 0);
task.State.Update<int>(INDEX, index => index + 1); task.State.Update<int>(INDEX, index => index + 1);
} }
var useAscii = context.LegacyConsole || !context.Unicode;
var sequence = useAscii ? _asciiSequence : _ansiSequence;
var index = task.State.Get<int>(INDEX); var index = task.State.Get<int>(INDEX);
return new Markup(sequence[index % sequence.Length].ToString(), Style ?? Style.Plain); return new Markup(spinner.Frames[index % spinner.Frames.Count], Style ?? Style.Plain);
} }
} }
} }

View File

@ -26,6 +26,12 @@ namespace Spectre.Console
/// </summary> /// </summary>
public bool AutoClear { get; set; } public bool AutoClear { get; set; }
/// <summary>
/// Gets or sets the refresh rate if <c>AutoRefresh</c> is enabled.
/// Defaults to 10 times/second.
/// </summary>
public TimeSpan RefreshRate { get; set; } = TimeSpan.FromMilliseconds(100);
internal List<ProgressColumn> Columns { get; } internal List<ProgressColumn> Columns { get; }
/// <summary> /// <summary>
@ -110,7 +116,7 @@ namespace Spectre.Console
if (interactive) if (interactive)
{ {
var columns = new List<ProgressColumn>(Columns); var columns = new List<ProgressColumn>(Columns);
return new InteractiveProgressRenderer(_console, columns); return new InteractiveProgressRenderer(_console, columns, RefreshRate);
} }
else else
{ {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console
{
/// <summary>
/// Represents a spinner used in a <see cref="SpinnerColumn"/>.
/// </summary>
public abstract partial class ProgressSpinner
{
/// <summary>
/// Gets the update interval for the spinner.
/// </summary>
public abstract TimeSpan Interval { get; }
/// <summary>
/// Gets a value indicating whether or not the spinner
/// uses Unicode characters.
/// </summary>
public abstract bool IsUnicode { get; }
/// <summary>
/// Gets the spinner frames.
/// </summary>
public abstract IReadOnlyList<string> Frames { get; }
}
}

View File

@ -15,9 +15,9 @@ namespace Spectre.Console.Internal
private readonly Stopwatch _stopwatch; private readonly Stopwatch _stopwatch;
private TimeSpan _lastUpdate; private TimeSpan _lastUpdate;
public override TimeSpan RefreshRate => TimeSpan.FromMilliseconds(100); public override TimeSpan RefreshRate { get; }
public InteractiveProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns) public InteractiveProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns, TimeSpan refreshRate)
{ {
_console = console ?? throw new ArgumentNullException(nameof(console)); _console = console ?? throw new ArgumentNullException(nameof(console));
_columns = columns ?? throw new ArgumentNullException(nameof(columns)); _columns = columns ?? throw new ArgumentNullException(nameof(columns));
@ -25,6 +25,8 @@ namespace Spectre.Console.Internal
_lock = new object(); _lock = new object();
_stopwatch = new Stopwatch(); _stopwatch = new Stopwatch();
_lastUpdate = TimeSpan.Zero; _lastUpdate = TimeSpan.Zero;
RefreshRate = refreshRate;
} }
public override void Started() public override void Started()

View File

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Spectre.Console.Internal; using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console.Rendering namespace Spectre.Console.Rendering
{ {