Skip to content

Instantly share code, notes, and snippets.

@phil-scott-78
Created January 28, 2025 04:51
Show Gist options
  • Save phil-scott-78/0d8a9918a65b6f56677104c6b75539e0 to your computer and use it in GitHub Desktop.
Save phil-scott-78/0d8a9918a65b6f56677104c6b75539e0 to your computer and use it in GitHub Desktop.
Spectre.Console Spinner
internal static class SpinnerExtensions
{
public static async Task Spinner(this Task task, Spinner spinner, Style? style = null, IAnsiConsole? ansiConsole = null)
{
await SpinnerInternal<object>(task, spinner, style, ansiConsole);
}
public static async Task<T> Spinner<T>(this Task<T> task, Spinner spinner, Style? style = null, IAnsiConsole? ansiConsole = null)
{
return (await SpinnerInternal<T>(task, spinner, style, ansiConsole))!;
}
private static async Task<T?> SpinnerInternal<T>(Task task, Spinner spinner, Style? style = null, IAnsiConsole? ansiConsole = null)
{
ansiConsole ??= AnsiConsole.Console;
style ??= Style.Plain;
var currentFrame = 0;
var cancellationTokenSource = new CancellationTokenSource();
// Start spinner animation in background
var spinnerTask = Task.Run(async () =>
{
while (!cancellationTokenSource.Token.IsCancellationRequested)
{
ansiConsole.Cursor.Show(false);
var spinnerFrame = spinner.Frames[currentFrame];
// Write the spinner frame
ansiConsole.Write(new Text(spinnerFrame, style));
ansiConsole.Write(new ControlCode(CUB(spinnerFrame.Length)));
currentFrame = (currentFrame + 1) % spinner.Frames.Count;
await Task.Delay(spinner.Interval, cancellationTokenSource.Token);
}
}, cancellationTokenSource.Token);
try
{
// Wait for the actual task to complete
if (task is Task<T> taskWithResult)
{
var result = await taskWithResult;
await cancellationTokenSource.CancelAsync();
await spinnerTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
return result;
}
else
{
await task;
await cancellationTokenSource.CancelAsync();
await spinnerTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
return default;
}
}
finally
{
var spinnerFrame = spinner.Frames[currentFrame];
ansiConsole.Write(new string(' ', spinnerFrame.Length));
ansiConsole.Write(new ControlCode(CUB(spinnerFrame.Length)));
ansiConsole.Cursor.Show();
await cancellationTokenSource.CancelAsync();
}
}
/// <summary>
/// This control function moves the cursor to the left by a specified number of columns.
/// The cursor stops at the left border of the page.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUB.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move backward.</param>
/// <returns>The ANSI escape code.</returns>
// ReSharper disable once InconsistentNaming
private static string CUB(int steps) => $"\e[{steps}D";
}
@phil-scott-78
Copy link
Author

use it like so

AnsiConsole.MarkupInterpolated($"  {r.GetType().Name}: ");
await r.Run(model, parameters).Spinner(Spinner.Known.Dots2, new Style(Color.Yellow));
AnsiConsole.MarkupLineInterpolated($"[green]Ok[/]");

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