DEV Community

Cover image for Spectre.Console helpers
Karen Payne
Karen Payne

Posted on

Spectre.Console helpers

Introduction

There are many uses for console projects that range from learning to code, utility and real applications. A standard console project requires a decent amount of code to collect information and to present information with colors.

The information here will assist with collecting user input, confirmation, presenting a generic list, colorizing json and exceptions. Bogus will be used to generate mock-up data and FluentValidation for validation of data to be inserted into an SQLite database using Dapper.

The focus will be on Spectre.Console which for most of the code has been placed into a class project. Within the class project the class SpectreConsoleHelpers has been setup as a partial class to separate various methods with useful names.

Source code

Spectre.Console documentation

Either before or after reviewing and running provided code, take time to view the documentation and clone the samples repository to better understand what is covered and not covered here.

Recommendations

Take time to review all code and if new to writing code, set breakpoints to step through code using the local window to examine values.

Also, no matter your level of experience there will be something in the provided source code to learn from.

Environment

  • Microsoft Visual Studio 2022 - 17.11.4
  • NET 8
  • C#
  • GlobalUsings.cs is used in both projects.

How ask a question

To ask for confirmation before continuing an operation code such as the following is used taken from this post.

bool confirmed = false;
string Key;
do {
    Console.Write("Please enter a login key: ");
    Key = Console.ReadLine();
    Console.WriteLine("You entered, " + Key + " as your login key!");

    ConsoleKey response;
    do
    {
        Console.Write("Are you sure you want to choose this as your login key? [y/n] ");
        response = Console.ReadKey(false).Key;   // true is intercept key (dont show), false is show
        if (response != ConsoleKey.Enter)
            Console.WriteLine();

    } while (response != ConsoleKey.Y && response != ConsoleKey.N);

    confirmed = response == ConsoleKey.Y;
} while (!confirmed);
Console.WriteLine("You chose {0}!", Key);
Console.ReadLine();
Enter fullscreen mode Exit fullscreen mode

With Spectre.Console, there is a method for asking a user for confirmation which in the sample below the method is wrapped in a reusable method that has colors set and a property for displaying a message when y/n or Y/N is not used. Also, the characters can be set to say ½ rather than y/n.

public static bool Question(string questionText)
{
    ConfirmationPrompt prompt = new($"[{Color.Yellow}]{questionText}[/]")
    {
        DefaultValueStyle = new(Color.Cyan1, Color.Black, Decoration.None),
        ChoicesStyle = new(Color.Yellow, Color.Black, Decoration.None),
        InvalidChoiceMessage = $"[{Color.Red}]Invalid choice[/]. Please select a Y or N.",
    };

    return prompt.Show(AnsiConsole.Console);
}
Enter fullscreen mode Exit fullscreen mode

This overload provides parameters to dynamically set colors.

public static bool Question(string questionText, Color foreGround, Color backGround)
{
    ConfirmationPrompt prompt = new($"[{foreGround} on {backGround}]{questionText}[/]")
    {
        DefaultValueStyle = new(foreGround, backGround, Decoration.None),
        ChoicesStyle = new(foreGround, backGround, Decoration.None),
        InvalidChoiceMessage = $"[{Color.Red}]Invalid choice[/]. Please select a Y or N."
    };

    return prompt.Show(AnsiConsole.Console);
}
Enter fullscreen mode Exit fullscreen mode

Usage

public static void PlainAskQuestion()
{
    if (Question("Continue with backing up database?"))
    {
        // backup database
    }
    else
    {
        // do nothing
    }
}
Enter fullscreen mode Exit fullscreen mode

Exiting an application

Usually done with Console.ReadLine();, with Spectre.Console the following can be using the following.

public static void ExitPrompt()
{
    Console.WriteLine();

    Render(new Rule($"[yellow]Press[/] [cyan]ENTER[/] [yellow]to exit the demo[/]")
        .RuleStyle(Style.Parse("silver")).LeftJustified());

    Console.ReadLine();
}

private static void Render(Rule rule)
{
    AnsiConsole.Write(rule);
    AnsiConsole.WriteLine();
}
Enter fullscreen mode Exit fullscreen mode

Usage

using static SpectreConsoleLibrary.SpectreConsoleHelpers;
internal partial class Program
{
    static void Main(string[] args)
    {
        ExitPrompt();
    }
}
Enter fullscreen mode Exit fullscreen mode

Getting user information

Many novice developers will ask for information as follows.

string firstName = Console.ReadLine();
string lastName = Console.ReadLine();
DateOnly birthDate = DateOnly.Parse(Console.ReadLine()!);
Enter fullscreen mode Exit fullscreen mode

The code is fine if the user enters the correct information but lets say the birthdate is not a DateOnly the program crashes. Next a developer may try using the following but that creates a new problem, how to ask for the date again cleanly?

string firstName = Console.ReadLine();
string lastName = Console.ReadLine();
string birthDate = Console.ReadLine();

if (DateOnly.TryParse(birthDate, out var bd))
{
    // date is valid
}
else
{
    // date is not valid
}
Enter fullscreen mode Exit fullscreen mode

With Spectre.Console, the following colorizes the prompt using private fields, is for returning a DateOnly and provides validation, in this case the year entered can’t before 1900. Note the validation is optional. If input is incorrect, a error message appears and re-ask for the DateOnly.

public static DateOnly GetBirthDate(string prompt = "Enter your birth date")
{
    /*
     * doubtful there is a birthday for the current person
     * but if checking say a parent or grandparent this will not allow before 1900
     */
    const int minYear = 1900;

    return AnsiConsole.Prompt(
        new TextPrompt<DateOnly>($"[{_promptColor}]{prompt}[/]:")
            .PromptStyle(_promptStyle)
            .ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter a valid date or press ENTER to not enter a date[/]")
            .Validate(dt => dt.Year switch
            {
                <= minYear => ValidationResult.Error($"[{_errorForeGround} on {_errorBackGround}]Year must be greater than {minYear}[/]"),
                _ => ValidationResult.Success(),
            })
            .AllowEmpty());
}
Enter fullscreen mode Exit fullscreen mode

Usage

Shown with two methods to collect string values

var firstName = SpectreConsoleHelpers.FirstName("First name");
var lastName = SpectreConsoleHelpers.LastName("Last name");
var birthDate = SpectreConsoleHelpers.GetBirthDate("Birth date");

Person person = new()
{
    FirstName = firstName,
    LastName = lastName,
    BirthDate = birthDate
};
Enter fullscreen mode Exit fullscreen mode

For FirstName, uses same colors as GetBirthDate and for demonstration does minor validation and has a custom error message for failing validation and on failing will re-ask for the first name.

    public static string FirstName(string prompt = "First name") =>
        AnsiConsole.Prompt(
            new TextPrompt<string>($"[{_promptColor}]{prompt}[/]")
                .PromptStyle(_promptStyle)
                .Validate(value => value.Length switch
                {
                    < 3 => ValidationResult.Error("[red]Must have at least three characters[/]"),
                    _ => ValidationResult.Success(),
                })
                .ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter your first name[/]"));

Enter fullscreen mode Exit fullscreen mode

The method to get last name validation is simple, the user must not leave the value empty.

public static string LastName(string prompt = "Last name") =>
    AnsiConsole.Prompt(
        new TextPrompt<string>($"[{_promptColor}]{prompt}[/]?")
            .PromptStyle(_promptStyle)
            .ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter your last name[/]"));

Enter fullscreen mode Exit fullscreen mode

Simple inputs

Here is an example to get an int were the method name is generic for demonstration only.

public static int GetInt(string prompt) =>
    AnsiConsole.Prompt(
        new TextPrompt<int>($"[{_promptColor}]{prompt}[/]")
            .PromptStyle(_promptStyle)
            .DefaultValue(1)
            .DefaultValueStyle(new(_promptColor, Color.Black, Decoration.None)));

Enter fullscreen mode Exit fullscreen mode

Generic inputs

We can use generics that T represents the type, a prompt to present the user with and a default value.

public static T Get<T>(string prompt, T defaultValue) =>
    AnsiConsole.Prompt(
        new TextPrompt<T>($"[{_promptColor}]{prompt}[/]")
            .PromptStyle(_promptStyle)
            .DefaultValueStyle(new(_promptStyle))
            .DefaultValue(defaultValue)
            .ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Invalid entry![/]"));
Enter fullscreen mode Exit fullscreen mode

Usage

string firstName = SpectreConsoleHelpers.Get<string?>("First name", null);
var lastName = SpectreConsoleHelpers.Get<string?>("Last name", null);
var birthDate = SpectreConsoleHelpers.Get<DateOnly?>("Birth date", null);
Enter fullscreen mode Exit fullscreen mode

Colorizing runtime exceptions

With Spectre.Console, AnsiConsole.WriteException writes an exception to the console. If a developer does not care for the default colors they can be customized as shown below done in a language extension.

public static class ExceptionHelpers
{
    /// <summary>
    /// Provides colorful exception messages in cyan and fuchsia
    /// </summary>
    /// <param name="exception"><see cref="Exception"/></param>
    public static void ColorWithCyanFuchsia(this Exception exception)
    {
        AnsiConsole.WriteException(exception, new ExceptionSettings
        {
            Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
            Style = new ExceptionStyle
            {
                Exception = new Style().Foreground(Color.Grey),
                Message = new Style().Foreground(Color.DarkSeaGreen),
                NonEmphasized = new Style().Foreground(Color.Cornsilk1),
                Parenthesis = new Style().Foreground(Color.Cornsilk1),
                Method = new Style().Foreground(Color.Fuchsia),
                ParameterName = new Style().Foreground(Color.Cornsilk1),
                ParameterType = new Style().Foreground(Color.Aqua),
                Path = new Style().Foreground(Color.Red),
                LineNumber = new Style().Foreground(Color.Cornsilk1),
            }
        });

    }
    /// <summary>
    /// Provides a colorful exception message
    /// </summary>
    /// <param name="exception"><see cref="Exception"/></param>
    public static void ColorStandard(this Exception exception)
    {
        AnsiConsole.WriteException(exception, new ExceptionSettings
        {
            Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
            Style = new ExceptionStyle
            {
                Exception = new Style().Foreground(Color.Grey),
                Message = new Style().Foreground(Color.White),
                NonEmphasized = new Style().Foreground(Color.Cornsilk1),
                Parenthesis = new Style().Foreground(Color.GreenYellow),
                Method = new Style().Foreground(Color.DarkOrange),
                ParameterName = new Style().Foreground(Color.Cornsilk1),
                ParameterType = new Style().Foreground(Color.Aqua),
                Path = new Style().Foreground(Color.White),
                LineNumber = new Style().Foreground(Color.Cornsilk1),
            }
        });

    }
}
Enter fullscreen mode Exit fullscreen mode

Usage

public static void ShowExceptionExample()
{

    AnsiConsole.Clear();
    SpectreConsoleHelpers.PrintHeader();

    try
    {

        // Create the InvalidOperationException with the inner exception
        throw new InvalidOperationException("Operation cannot be performed", 
            new ArgumentException("Invalid argument"));
    }
    catch (Exception ex)
    {
        ex.ColorWithCyanFuchsia();
    }
}
Enter fullscreen mode Exit fullscreen mode

Shows colorize exception

Colorized json

This is done with the following method which in this case overrides the default colors.

Requires Spectre.Console.Json NuGet package.

public partial class SpectreConsoleHelpers
{
    public static void WriteOutJson(string json)
    {
        AnsiConsole.Write(
            new JsonText(json)
                .BracesColor(Color.Red)
                .BracketColor(Color.Green)
                .ColonColor(Color.Blue)
                .CommaColor(Color.Red)
                .StringColor(Color.Green)
                .NumberColor(Color.Blue)
                .BooleanColor(Color.Red)
                .MemberColor(Color.Wheat1)
                .NullColor(Color.Green));
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage with mocked json

static void DisplayJsonExample()
{

    AnsiConsole.Clear();
    SpectreConsoleHelpers.PrintHeader();

    SpectreConsoleHelpers.WriteOutJson(Json);

}

public static string Json =>
    /*lang=json*/
    """
    [
      {
        "FirstName": "Jose",
        "LastName": "Fernandez",
        "BirthDate": "1985-01-01"
      },
      {
        "FirstName": "Miguel",
        "LastName": "Lopez",
        "BirthDate": "1970-12-04"
      },
      {
        "FirstName": "Angel",
        "LastName": "Perez",
        "BirthDate": "1980-09-11"
      }
    ]
    """;
Enter fullscreen mode Exit fullscreen mode

Shows colorized json

Showing progress

Using the Status class provides a way to show progress of one or more operations.

Stock example

private static void SpinnerSample()
{
    AnsiConsole.Status()
        .Start("Thinking...", ctx =>
        {
            // Simulate some work
            AnsiConsole.MarkupLine("Doing some work...");
            Thread.Sleep(1000);

            // Update the status and spinner
            ctx.Status("Thinking some more");
            ctx.Spinner(Spinner.Known.Star);
            ctx.SpinnerStyle(Style.Parse("green"));

            // Simulate some work
            AnsiConsole.MarkupLine("Doing some more work...");
            Thread.Sleep(2000);
        });
}
Enter fullscreen mode Exit fullscreen mode

A realistic example using EF Core to recreate a database.

public static class EntityExtensions
{
    /// <summary>
    /// Recreate database with spinner
    /// </summary>
    public static void CleanStart(this DbContext context)
    {
        AnsiConsole.Status()

            .Start("Recreating database...", ctx =>
            {

                Thread.Sleep(500);

                ctx.Spinner(Spinner.Known.Star);
                ctx.SpinnerStyle(Style.Parse("cyan"));

                ctx.Status("Removing");
                context.Database.EnsureDeleted();

                ctx.Status("Creating");
                context.Database.EnsureCreated();

            });

    }
}
Enter fullscreen mode Exit fullscreen mode

Recording actions

To record information entered by users start with AnsiConsole.Record() and save to HTML file via await File.WriteAllTextAsync("recorded.html", AnsiConsole.ExportHtml()); which can be helpful in figuring out an issue while creating an application.

Other options

  • AnsiConsole.ExportText()
  • AnsiConsole.ExportCustom()

Summary

With information and provide code sample for Spectre.Console a developer can write more robust console projects.

Top comments (0)