Crossterm Shapes
Lately I have been getting back into writing more rust code. One thing that has my interest in Rust is getting back into playing with terminal user interfaces and drawing graphics in a terminal. As a dip back into this after not playing with it in many months, I have written a little program to draw some basic shapes in a terminal instance. I think eventually it would be fun to play around with building on top of making some basic shapes into creating a my own terminal user interface library. However, that can be a bit of a lofty goal. First, start with some basics.
Crossterm is a multiplatform terminal manipulation library and makes it easy to do some interesting things in the terminal. It provides some nice abstractions so that we don’t have to get too deep in the weeds on terminals, which I have done before in C. That can be fun, but not necessarily when you are just looking to play around. But to give a taste of doing this in a more low level way, you often have to use a series of codes to accomplish tasks such as changing the color of text in a terminal or moving around. These are usually ansi escape codes. And it might look a little something like this:
For information on something such as color code, I would refer you to take a look at this.
Luckily, crossterm provides some nice easy ways of dealing with this without all of that.
The code shown today will be able to accomplish a few things:
First to do is to grab a struct of Stdout.
According to the documentation, this is a handle on the global standard output stream of the current running process. In the case of our program, it is the terminal. We do this in the first like but evoking the stdout()
function and setting it equal to a mutable stdout
variable. The next thing we may want to do is to clear the terminal of pesky text we don’t want. Crossterm gives us a nice function that can be used on stdout called execute. This allows for executing commands on the stdout. We can pass a command provided in terminal
called Clear
. Clear allows the terminal buffer to cleared, however it can take enums that communicate how to clear it. In this case, we want to clear everything, so we can use ClearType::All.
However, there are many different ways to clear the terminal. We can clear from cursor down, cursor up, only the current line and a few more options.
Next thing we may want are some bounds on our terminal. We can get this using terminal::size. This will return a tuple that we can unpack into their respective x and y coordinates. It should also be noted that main returns a Result
and the ?
operator pretty much says if an error happens to bubble this up/bail out of the program.
Then, want to enter into raw mode. Reading the documentation, raw mode will set the following modes:
write!
.Following that is our main loop where we will process some inputs and react based on that. At the end, we return Ok(())
if we are exiting successfully.
The enum KeyBinding
is a set of conditions we intend to handle. When the input process stage happens, it will try to gather input. Depending on this input, input processing will return an enum for the operation to conduct. The operations are fairly straigh forward. None
is returned if there was no input to process. Clear
will clear the terminal screen. Quit
is how from the terminal we communicate that we are quitting. Square
and Trianglel
to draw those respective shapes on the terminal.
Here is the meat of input processing. Using Event and event. Event
is an enum with Key
being the enum for key events. Key
contains a KeyEvent. To get this, event::read()
is called to read any events in the queue. Afterwards, matching on the type of KeyEvent
. KeyEvent
is a struct with several data members, a KeyCode
, KeyModifier
, KeyEventKind
, and KeyEventState
. We only need two here. So, match on the KeyEvent with the specific codes (of type KeyCode) and modifiers (of type KeyModifiers). The assignments are as follows:
| KeyEvent | Key binding |
|:---------|:------------|
| Quit | Ctrl + q |
| Square | s |
| Triangle | t |
| Clear | c |
In all other events, return a None
Keybinding.
For this program, points will be printed using an asterik (*). But, before printing shapes, the cursor must be moved. This is handled in the following function.
Calling execute on stdout
using the crossterm::cursor functions. This will allow a y and x to be passed in and the cursor will move to that location.
The following function will be called to print a point.
Print is located in crossterm::style
and allows for some simple and useful functionality. It will take an argument that implements Display
and functions can be called on that to add some extra features like .blue()
will take whatever is being printed and print it out as blue.
These functions will print the shapes respectively. I demonstrated two different ways. The square is a little algorithm that traces out a square based on the bounds provided with the sides computed. The triangle is a little more manual and demonstates how a 2-D array (vectors in Rust’s specific case) can be used to draw to the screen. If one wanted to maybe make some more complicated drawing, some functionality could be built in to read in a text file with a shape drawn in it and use that as a base for what to draw.