How YOU can build a great looking .NET Console app with Spectre

Chris Noring - Feb 17 '22 - - Dev Community

This article will show how to build a great looking console app that your users will love interacting with. The console app will contain some graphical widgets that will make for a great user experience.

Why console apps

There are many types of apps, mobile apps, web apps, console apps, games and so on. Many of them have really great looking UIs some doesn't need UIs to run. So, is there a need for console apps, apps that run in your terminal with seemingly little or no graphical interface? In short yes, here's some cases:

  • Scripting. First of all, not all apps require a user to function. For example, as part of a CI/CD pipeline there's an agent that performs various steps. One of those steps could be executing an app. Such an app is usually a console app that may take command line arguments as data input, and operate on those. Here, there shouldn't be a graphical interface, as it would stop everything up and wait for a user to interact.
  • Batch processing. There are many apps out there that are really good at working on batches of data and all it wants from you is for you to provide input and it does the rest. That input can be a file directory, a URL or something else, no need for a user to click a button.

It's a console app, it's ok if the UI is bad?

It's easy to have the opinion that a console app doesn't need a good-looking interface, especially if it's of the scripting or batch processing types we mentioned above. However, some console apps, might warrant a better-looking interface if a user is meant to interact with it. From a user's standpoint, what would the requirements be of a such an interface? Here's some ideas:

  • Correctness. As a user you need the program to help you input the correct data, that could be the correct name of things, or from a limited list of options and so on.
  • Feedback. As a user, you want to know that the program has correctly understood my input but also that it's currently working on something and when it's done "doing its thing". What you don't want is a program that says nothing, as you might interpret that as malfunctioning or that you need to sit and watch it until it finishes
  • Read support. You want to help the user by highlighting certain keywords, especially if you plan to describe something in text. This ensures the user understands what they're supposed to do.

Ok, so we have a case for why we should care about the user interface of a console app, what are my options?

In this article, we will look at Spectre.Console, a very competent library for helping us not only with the user experience with various graphical widgets but that also contain methods for helping us process command-line input.

References

Here's interesting links that might help you to learn more:

Spectre, a high-level view

At high-level, Spectre.Console contains two major things:

  • Graphical widgets, Spectre.Console. Here we have features like:
    • Prompts, this is a great feature as it allows you to prompt the user for input, both single input and selection/s from a list
    • Status, Progress. By having these, you're able to convey the user how the program is doing, so you know whether you can leave the program to do it's thing if it's working or if it's stuck.
    • Coloration, this allows you to set certain colors to the text to ensure you can differentiate between different types of messages like errors, logs and also highlight keywords.
    • Widgets, Table, Tree, BarCharts and even a Canvas. Anyone wants to build Pacman in the console, here's your chance :)
  • Command line processing, Spectre.Console.Cli

Exercise - Hello Spectre

Here, we will start with the example highlighted in the quick start. It may look simple, but what you're getting is quite useful.

  1. Run dotnet new in your terminal to scaffold a dotnet project.


   dotnet new console -o spectre-demo
   cd spectre-demo


Enter fullscreen mode Exit fullscreen mode
  1. Run dotnet add package to add the Spectre.Console package to your project:


   dotnet add package Spectre.Console


Enter fullscreen mode Exit fullscreen mode
  1. Add the following code to your Program.cs:

    1. At the top, add using directive:


   using Spectre.Console;


Enter fullscreen mode Exit fullscreen mode
  1. Lower down, add these lines:


   AnsiConsole.Markup("[underline red]Hello[/] World!");

   AnsiConsole.Markup("[green]This is all green[/]");


Enter fullscreen mode Exit fullscreen mode

Now you have a program using Spectre.Console. Lets run it next, to see what it does:

  1. Run dotnet run, to build and run your app:


   dotnet run


Enter fullscreen mode Exit fullscreen mode

You should see an output looking like this text:

Spectre output

What you are seeing is Spectre's ability to color the output. It works in the following way:

  1. You call the Markup() method. It expects markup that it can turn into colors. The markup is defined like so:


   [color format]some text[/]


Enter fullscreen mode Exit fullscreen mode

Here's an example:



   AnsiConsole.Markup("[underline red]Hello[/] World!");


Enter fullscreen mode Exit fullscreen mode

The above text "Hello" will be made "underlined" and "red".

Exercise - build a scaffolder app

So far, you've learned how you can add colors and other formatting, to part of a string.

User input

There's more, you can also capture user input. For that you will use the Confirm() method. Confirm() will present itself as an input field asking the user for 'y' or 'n'. The response will be stored as a boolean. Here's an example:



bool saveFile = AnsiConsole.Confirm("Save file?");


Enter fullscreen mode Exit fullscreen mode

Let's decide on working on a bit more advanced project, so clear all previous code and let's start fresh. Our project will be able to scaffold files we might need for a GitHub repo.

  1. Add the following code to prompt the user:


   var answerReadme = AnsiConsole.Confirm("Generate a [green]README[/] file?");

   var answerGitIgnore = AnsiConsole.Confirm("Generate a [yellow].gitignore[/] file?");


Enter fullscreen mode Exit fullscreen mode

Great, now lets see what that looks like, rerun with dotnet run:

prompt

Ok, great, we can capture user input.

Capture user input from a selection

Sometimes you want the user to select from a list of options. This is to ensure the user provides valid input. For this, you can use Prompt(). The idea is to provide a list of choices. The selected choice is returned to you.



var framework = AnsiConsole.Prompt(
  new SelectionPrompt<string>()
      .Title("Select [green]test framework[/] to use")
      .PageSize(10)
      .MoreChoicesText("[grey](Move up and down to reveal more frameworks)[/]")
      .AddChoices(new[] {
          "XUnit", "NUnit","MSTest"
      }));


Enter fullscreen mode Exit fullscreen mode

Let's add the above code to our project, i.e the Program.cs:



using Spectre.Console;

Console.WriteLine("");
var answerReadme = AnsiConsole.Confirm("Generate a [green]README[/] file?");
var answerGitIgnore = AnsiConsole.Confirm("Generate a [yellow].gitignore[/] file?");

var framework = AnsiConsole.Prompt(
    new SelectionPrompt<string>()
        .Title("Select [green]test framework[/] to use")
        .PageSize(10)
        .MoreChoicesText("[grey](Move up and down to reveal more frameworks)[/]")
        .AddChoices(new[] {
            "XUnit", "NUnit","MSTest"
        }));


Enter fullscreen mode Exit fullscreen mode

Running the program with dotnet run, you should see something like:

prompt selection list

Adding a program Figlet

Let's add a figlet, a big header that make it feel like a real app.

  1. Just below the using directives, add this code:


   AnsiConsole.Write(
    new FigletText("Scaffold-demo")
        .LeftAligned()
        .Color(Color.Red));


Enter fullscreen mode Exit fullscreen mode

Your full code so far should look like so:



   using Spectre.Console;

   AnsiConsole.Write(
    new FigletText("Scaffold-demo")
        .LeftAligned()
        .Color(Color.Red));

    Console.WriteLine("");
    var answerReadme = AnsiConsole.Confirm(
     "Generate a [green]README[/] file?");
    var answerGitIgnore = AnsiConsole.Confirm(
     "Generate a [yellow].gitignore[/] file?");


Enter fullscreen mode Exit fullscreen mode

Try running it with dotnet run:

Figlet added

Alright, now that's better, it almost like something we can be proud of :)

Processing user input with Status()

Next, we want to add code to show how we are processing the user input, for that we can use Status(). It will show as a spinner and yuu can add text next to the spinner, real fancy I promise :).

Status() roughly works like so:



AnsiConsole.Status()
    .Start("Generating project...", ctx =>
    {
      // define tasks to perform
      // define spinner type
      // define spinner color
      // define delay between tasks
    })


Enter fullscreen mode Exit fullscreen mode

I've written some code comments in the above code to say what we need to do. We need to add a spinner that conveys to the user that tasks are being performed. Also, the user is used to things not being instantaneous, so it's a good idea to add a slight delay between the tasks.

Now, let's keep working on our project.

  1. Add the following code at the bottom:


   AnsiConsole.Status()
    .Start("Generating project...", ctx =>
    {
      if(answerReadme) 
      {
        AnsiConsole.MarkupLine("LOG: Creating README ...");
        Thread.Sleep(1000);
        // Update the status and spinner
        ctx.Status("Next task");
        ctx.Spinner(Spinner.Known.Star);
        ctx.SpinnerStyle(Style.Parse("green"));
      }

      if(answerGitIgnore) 
      {
        AnsiConsole.MarkupLine("LOG: Creating .gitignore ...");
        Thread.Sleep(1000);
        // Update the status and spinner
        ctx.Status("Next task");
        ctx.Spinner(Spinner.Known.Star);
        ctx.SpinnerStyle(Style.Parse("green"));
      }

      // Simulate some work
      AnsiConsole.MarkupLine("LOG: Configuring test framework...");
      Thread.Sleep(2000);
    });


Enter fullscreen mode Exit fullscreen mode

Let's zoom in a on a piece of code:



if(answerReadme) 
{
  AnsiConsole.MarkupLine("LOG: Creating README ...");
  Thread.Sleep(1000);
  // Update the status and spinner
  ctx.Status("Next task");
  ctx.Spinner(Spinner.Known.Star);
  ctx.SpinnerStyle(Style.Parse("green"));
}


Enter fullscreen mode Exit fullscreen mode
  • AnsiConsole.MarkupLine(), this types a log message at the start of the task
  • Thread.Sleep(1000);, this ensures there's a pause between tasks
  • ctx.Status("Next task");, this is the text shown next to the spinner
  • The following code defines spinner type and spinner color:


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


Enter fullscreen mode Exit fullscreen mode

Ok, your code should now look like so:



using Spectre.Console;

AnsiConsole.Write(
new FigletText("Scaffold-demo")
    .LeftAligned()
    .Color(Color.Red));

Console.WriteLine("");
var answerReadme = AnsiConsole.Confirm("Generate a [green]README[/] file?");
var answerGitIgnore = AnsiConsole.Confirm("Generate a [yellow].gitignore[/] file?");

Console.WriteLine("");
var answerReadme = AnsiConsole.Confirm("Generate a [green]README[/] file?");
var answerGitIgnore = AnsiConsole.Confirm("Generate a [yellow].gitignore[/] file?");

var framework = AnsiConsole.Prompt(
  new SelectionPrompt<string>()
      .Title("Select [green]test framework[/] to use")
      .PageSize(10)
      .MoreChoicesText("[grey](Move up and down to reveal more frameworks)[/]")
      .AddChoices(new[] {
          "XUnit", "NUnit","MSTest"
      }));

AnsiConsole.Status()
  .Start("Generating project...", ctx =>
  {
    if(answerReadme) 
    {
      AnsiConsole.MarkupLine("LOG: Creating README ...");
      Thread.Sleep(1000);
      // Update the status and spinner
      ctx.Status("Next task");
      ctx.Spinner(Spinner.Known.Star);
      ctx.SpinnerStyle(Style.Parse("green"));
    }

    if(answerGitIgnore) 
    {
      AnsiConsole.MarkupLine("LOG: Creating .gitignore ...");
      Thread.Sleep(1000);
      // Update the status and spinner
      ctx.Status("Next task");
      ctx.Spinner(Spinner.Known.Star);
      ctx.SpinnerStyle(Style.Parse("green"));
    }

    // Simulate some work
    AnsiConsole.MarkupLine("LOG: Configuring test framework...")
;
    Thread.Sleep(2000);
  });


Enter fullscreen mode Exit fullscreen mode

Let's take it for a spin with dotnet run. You should see something like:

demo

Exercise - install globally, run everywhere

Today you might consume apps from NuGet. However, some apps might not be something you want to share as it might be company internal, or they are personal projects. Is there a way to install things locally? In short, yes. We can pack up it up as NuGet package and install globally on our machine, this will make it available anywhere on your machine.

To make install locally, we need to perform the following steps:

  • Tell our project file it needs to be packed as a tool
  • Pack the program into a tool
  • Install globally on our machine
  • Run the app from anywhere :)

Configure project file

We need to tell our project it's a tool.

  1. Open spectre-demo.csproj and locate <PropertyGroup> and add the following elements:


   <PackAsTool>true</PackAsTool>
   <ToolCommandName>spectre-demo</ToolCommandName>
   <PackageOutputPath>./nupkg</PackageOutputPath>


Enter fullscreen mode Exit fullscreen mode

these elements will identify our app as a tool, give it a name "spectre-demo" and define an output path where the package will end up

  1. In a terminal, run dotnet pack to create a package:


   dotnet pack


Enter fullscreen mode Exit fullscreen mode
  1. Install globally by running below command:


   dotnet tool install --global --add-source ./nupkg spectre-demo


Enter fullscreen mode Exit fullscreen mode

And we've done it! Let's test it out by navigating to any directory on your machine and run spectre-demo. That should start your app. Congrats!

I hope you liked this tutorial. Please comment if you want to see a part 2. Thanks for reading :)

Summary

We discussed console apps and why we need them.

You were then introduced to Spectre.Console, a very competent NuGet package giving us all kinds of capabilities from graphical widgets to command line processing.

You also learned how to use some of these widgets and install your program as a global command.

Here's a repo

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .