A .NET TUI for Advent of Code

yeehawtoast

Christopher Smith

Posted on November 22, 2024

A .NET TUI for Advent of Code

It's that time of year in North America: wood stoves are burning, lights are going on houses, turkey is being put in the refrigerator, and weirdo nerds like myself are awaiting 12am, December 1st. That is when the first Advent of Code puzzle drops.

Most people are healthy and well-adjusted or came from good, loving families, so they do it in python. However, I was raised in a corporate cubicle in some nameless office deep in the Texas countryside, so I'm doing my Advent of Code in C#.

Why C#? Firstly, it's one the most common language I use at work. I could do it in SAP's ABAP, but 1. I don't hate myself and 2. God no. I could do it in Go or Python, but considering neither of those pay my bills, I'll stick to C#.

Finally, Jetbrains just made Rider free for non-commercial use and considering Rider works on *nix systems, I'm happy to stick with C# for now.

This series will teach you about building TUIs, how they work, and how to build one in C versus how one might build one in say, Go or Python.

What is a TUI

A Terminal User Interface is what it says on the tin: A way to run an graphical application or program from the terminal. Neovim is a perfect example of a TUI.

Why build a TUI? Because it's fun, a unique way to present graphical information, gives a retro feels, and I don't have to worry about whether you are using Windows, Apple, KDE, Xorg, or Wayland. If its in the terminal, it can be run just about anywhere.

So, let's get started!

Objective 1: Building the initial terminal application

Start by opening up Rider, Visual Studios, VS Code or what have you, and creating a new solution. Put it somewhere, give it a name and make sure it is a console application. You could make it a blank solution, but the console template starts you off with console related basics.

Once you've done that, you'll get a blank screen like this:

Image description

From here, we will write our masterpiece. You'll notice the

Public Class Program
{
     Public Static Void Main(string args[])
     {

     }

}
Enter fullscreen mode Exit fullscreen mode

Is missing and that's because .NET now allows you to remove top level statements from programs where you are running your main method. Instead of being added later, the compiler assumes you are starting from Program thus allows you to just start writing code, the way you would with any non-oop-based language.

Anywho, this won't impact our ability to write our TUI.

Next, we need a framework for writing a TUI and there's plenty of C# options out there. You can use whatever you want, I'm not your father, but the one I will be using is terminal.gui because of its maturity, robustness, and overall completeness.

There are 2 ways to install it: directly from your IDE and from the command line.

Option 1: IDE
In your IDE, right click on your Solution and click "Manage NuGet Packages." in the search box that pops up, search for terminal.gui and install it. Boom. Done.

Option 2: CLI interface.

CD to your directory. Type:
dotnet add package Terminal.Gui
and hit enter. Boom. Done.

If you can't tell, I prefer the CLI way, but not everyone does. Pick what works for you.

After this, we are ready to start building.

Hello, World: Our first TUI application

Everything in terminals.gui is about views. You could say that it is the V part of M.V.C and you could, in theory, build a whole M.V.C. application around this idea (spoilers) but for now, we are just looking at views and the front end.

For now, go ahead and type this code line by line as I explain what is happening:

using Terminal.Gui;

// Initialize the application
Application.Init();
var top = Application.Top;
Colors.Base.Normal = new Terminal.Gui.Attribute(Color.White, Color.Black);

// Create the main window
var mainWindow = new Window("Ho Ho Ho World!")
{
    X = 0,
    Y = 0,
    Width = Dim.Fill(),   // Use the full width of the terminal
    Height = Dim.Fill()   // Use the full height of the terminal
};

// Add a label to the main window
var label = new Label("Ho Ho Ho, Hello World!")
{
    X = Pos.Center(),     // Center the label horizontally
    Y = Pos.Center()      // Center the label vertically
};
mainWindow.Add(label);

var btn = new Button("Ho Ho Ho, Goodbye World!")
{
    X = Pos.Center(),
    Y = Pos.Bottom(label)
};
mainWindow.Add(btn);
btn.Clicked += () =>
{
    Application.RequestStop();
};

// Add the main window to the application
Application.Top.Add(mainWindow);

// Run the application
Application.Run();

// Optional: Clean up when the application exits
Application.Shutdown();

Enter fullscreen mode Exit fullscreen mode

You can probably already get quite a bit from the comments I've added into the code, but we'll still go ahead and explain line by line.

Like with any c# program, we need to tell the compiler we are are using terminal.gui and want to use its library as part of our codebase.

Before anything can be displayed, we need to initialize our application with Application.Init() which allows us to start defining our program. Loyal SAP ABAPers will know this idea from building a local class definition, then initializing it, then using START-OF-SELECTION to run the program using the selected data from the selection screen.

More specifically Init() sets up the terminal for use by your program, sets up the view hiearchy and prepares the canvas to have the views, items, and details added.

One other thing it does is initialize a TopLevel, which is the logical view, which houses all other components and views.

Finally, after Application.Run is called, it creates and builds the views according to the programming logic you come up with.

After doing this we set a variable to the value of Application.Top which is just an instance of our TopLevel view

Building the Screen

From here, we are building our elements that make up our initial view. We have our colors, and then we create a new window (which is just the container things go in) and finally we are creating labels and buttons. We have an action for the button, which is requesting that we shutdown the program when the button is clicked).

When we are done designing our components we use mainwindow.Add to add them to a specific window or view.

When we are done building our window, we use top.Add(mainwindow) to add our window the top level view and add Application.Run to tell the compiler "Hey! Here's everything we want to have happen, go ahead and execute everything!"

One last note: To help make sure our program ends the right way, we have Application.Shutdown which handles cleaning up the view, garbage collecting the memory and releasing the terminal, so that nothing breaks.

Wrapping up

There's a lot that is happening in this very small program, but its important to know so that when we mess up somewhere, we can have a good foundation in how the whole program functions.

In part 2, we'll start to add decoration, show how to add a second screen and start to talk about stateful behavior in our TUI. Hold onto your butts, it's about to get fun!

💖 💪 🙅 🚩
yeehawtoast
Christopher Smith

Posted on November 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

A .NET TUI for Advent of Code
csharp A .NET TUI for Advent of Code

November 22, 2024