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.
- Run
dotnet new
in your terminal to scaffold a dotnet project.
dotnet new console -o spectre-demo
cd spectre-demo
- Run
dotnet add package
to add the Spectre.Console package to your project:
dotnet add package Spectre.Console
-
Add the following code to your
Program.cs
:- At the top, add using directive:
using Spectre.Console;
- Lower down, add these lines:
AnsiConsole.Markup("[underline red]Hello[/] World!");
AnsiConsole.Markup("[green]This is all green[/]");
Now you have a program using Spectre.Console. Lets run it next, to see what it does:
- Run
dotnet run
, to build and run your app:
dotnet run
You should see an output looking like this text:
What you are seeing is Spectre's ability to color the output. It works in the following way:
- You call the
Markup()
method. It expects markup that it can turn into colors. The markup is defined like so:
[color format]some text[/]
Here's an example:
AnsiConsole.Markup("[underline red]Hello[/] World!");
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?");
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.
- 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?");
Great, now lets see what that looks like, rerun with dotnet run
:
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"
}));
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"
}));
Running the program with dotnet run
, you should see something like:
Adding a program Figlet
Let's add a figlet, a big header that make it feel like a real app.
- Just below the using directives, add this code:
AnsiConsole.Write(
new FigletText("Scaffold-demo")
.LeftAligned()
.Color(Color.Red));
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?");
Try running it with dotnet run
:
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
})
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.
- 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);
});
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"));
}
-
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"));
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);
});
Let's take it for a spin with dotnet run
. You should see something like:
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.
- Open spectre-demo.csproj and locate
<PropertyGroup>
and add the following elements:
<PackAsTool>true</PackAsTool>
<ToolCommandName>spectre-demo</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
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
- In a terminal, run
dotnet pack
to create a package:
dotnet pack
- Install globally by running below command:
dotnet tool install --global --add-source ./nupkg spectre-demo
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
Top comments (5)
Looking forward to part 2 :)
I appreciate all the hard work that has gone into this, but I am rather amused by this for a number of reasons.
When I first started programming, one of the tools I used was QuickBasic version 4.5.
There were hundreds of libraries available for it and many were for the user interface, a console user interface, since this was still in the DOS days.
To make use of colour, you had to include ANSI.SYS in the config.sys file.
This project is a reflection of what those libraries offered, back in the late 80's and early 90's.
It always amuses me how progress sometimes eats its own tail.
There were complete ide programming,
Turbo C, Turbo Pascal...
Very interesting thanks a lot! The demo link doesn't seem to work by the way.
it doesn't render gif well, I've replaced it for a png.. If you want to see a gif, please look at the repo, github.com/softchris/spectre-demo Thanks.