My experience with computers (and text-based graphical user interfaces) began as a kid back in the eighties at a friend’s birthday party. It was a “brand-new” Amstrad CPC with 64kb memory and a cassette tape drive. Turning it on for the first time was a special moment, to say the least, (and funny too!) as we were eventually left with a green static screen and a command prompt, with everyone in the room looking at each other clueless about what we had to do.
No colors, no pictures or fancy graphics and for some people that might have been a dark and scary experience, but for others that “stiff” text-based GUI, was the foundation of the future. Learning how to use that computer (and finally making it play games, of course) revealed a whole new world for us. Dated technology may not look as impressive now as it did back then, but it certainly had an enormous impact on the evolution of modern technology.
Graphics chips for video games have been around since the 1970s; specially designed for the glorious arcade video systems.
Entering the Text-Zone
Software developers have a broad arsenal of GUI libraries and frameworks but there are times you’d just need your program to run on a standard console. On the other hand, programming a waterfall of print statements wouldn’t be an option; you’d like to build something a bit more advanced but without leaving the text zone. Unfortunately, nobody guarantees that an application driven by a text GUI will be consistent across different terminals and systems (and that was true especially back in the day) although most terminals now identify themselves as xterm (the X Window System terminal emulator) through the TERM environment variable.
Visualizing our goal
We are using a Java library called Lanterna for creating this text-based terminal GUI in the context of a temperature monitoring system. Lanterna allows us to write easy semi-graphical user interfaces in a text-only environment, very similar to the C library curses but with more functionality.
“Lanterna is supporting xterm compatible terminals and terminal emulators such as konsole, gnome-terminal, putty and many more. One of the main benefits of lanterna is that it’s not dependent on any native library but runs 100% in pure Java.”
Project analysis and source-code
I’m using Apache Maven to manage this Java-based project and bring it to harmony with Lanterna 3. Lanterna is available on Maven Central, through Sonatype OSS hosting. Now let’s take a quick look at our project’s packages and files.
- tms: This is where the class containing our main method lives and makes all the necessary preparations, like instantiating some core classes and creating a couple of threads; it brings everything to life.
- gui: Holds the class responsible for casting those powerful Lanterna spells and involves the displaying and assessment of the temperature monitoring system’s data.
- physics: Contains the code for simulating and controlling ambient temperature changes to be picked up by the system’s heat sensors.
- Inside the other packages, we have the core functionality of our temperature monitoring application. The idea here is that we have a heat sensor monitoring system with all sensors placed at different locations.
Note: Including too many screenshots in this article would be an overkill; for the complete source code please visit the repository on GitHub.
Lanterna’s layers and the Screen interface.
Lanterna provides three layers to choose from; a low-level terminal interface, a full-screen buffer and an out-of-the-box GUI toolkit with ready to use components such as buttons, windows, text boxes etc. In this project, we won’t be using a low-level terminal, neither the full GUI toolkit, but instead, we’ll be designing a custom text GUI with a full-screen buffer by using the Screen interface.
Screen is a fundamental layer in Lanterna, providing a preview surface representation of the terminal, where you can perform precise operations to a back-buffer in memory, before applying the changes to the actual terminal by invoking a refresh method. Lanterna’s Screen also tracks terminal content through a front-buffer as long as the terminal is modified internally.
Our LanterminalEngine class
This class implements Lanterna’s text-based GUI functionality. Notice the three member variables at the top, with the first one being of type TemperatureMonitoringSystem. Although this field has nothing to do with Lanterna, its declaration emphasizes the importance of having an object capable of streaming sensor temperature data.
Responsible for the actual graphical user interface implementation is the field of type Screen, imported from the Lanterna library. Coming also from Lanterna is TextColor, an abstract base class for terminal color definitions. Our text-GUI journey begins by invoking that createScreen() method inside the initialize() declaration. Notice that we’re setting our primary colors using Enum constants that represent ANSI colors.
Creating a Screen with DefaultTerminalFactory
Even if we’re not directly using a low-level terminal, the Screen layer needs to get hold of a Terminal object as its target. There’s a small number of terminal implementations available (like UnixTerminal, SwingTerminal and TelnetTerminal for example) and even though we could explicitly use one of these, we’ll be using the DefaultTerminalFactory class, which uses a detection mechanism for automatically creating the right terminal implementation based on the running system’s characteristics. Here’s a screenshot of our program in a SwingTerminalFrame running through our IDE (Spring Tool Suite) on Manjaro Linux.
For all systems with a graphical environment present, the Swing Terminal Frame will be chosen. You can also use it in case your IDE doesn’t implement the ANSI escape codes correctly.
The Temperature Monitoring System and the Sensor Interface
Our temperature monitoring system requires some sensor objects with each sensor requiring an ambient temperature instance to measure heat from.
And the sensor implementation:
An ambient temp sensor isn’t aware of any locations, coordinates or any names; all it needs is an instance of AmbientTemperature. As for the implemented interface, it’s pretty straightforward; measures heat from an AmbientTemperature object and provides a getTemperature() method to be available for the TemperatureMonitoringSystem.
We will need to initialize these ambient temperatures with some valid values while also being able to make random minor changes to them over time. This can be achieved with a threaded AmbiTemperRegulator:
And here is the simulate() method; the heart of regulator’s behavior:
Inside the simulation block we’re using one of Java’s random number generators, by invoking two different methods on the current thread; one which returns a pseudo-random integer value between 0 and 1 (2 is an exclusive specification) and another which returns a double between 0.0 and N times the value of the offset variable. This simple algorithm simulates temperature changes and prevents values getting out of control. Also, we’re using method synchronization when initializing the ambient temperatures for preventing thread interference and memory consistency errors.
Casting Lanterna Spells: Drawing our custom GUI
Before we start, we need to make sure that no cursor is visible when drawing our GUI, by using Screen’s setCursorPosition() method and passing “null”. Lanterna will attempt to hide the cursor if the terminal implementation supports it. We’re also assigning the screen dimensions -as they’re represented by the buffer- to the width and height integer variables, for the sake of clarity when using long statements. It’s also nice to know that we don’t have to worry about pixels here because it’s all about rows and columns!
In order to perform our draw operations, we need to get hold of a TextGraphics object out of our Screen. Any operations executed on this object will not be visible until a refresh() method is invoked. Also, there is support for method chaining which is something especially useful when drawing text graphics.
- Calling the drawLine() method on our TextGraphics object draws a line from a specified screen position to another, using a supplied character (can also specify additional metadata such as colors and modifiers by using a TextCharacter object). Specifically, we’re printing a simple space character with inverted coloration, giving the illusion of a header and a footer, each taking a screen space of just one row.
- Calling the putCSIStyledString() method puts a String on the screen at a specified position. There is also a simpler putString() method that doesn’t support the extra functionality of embedding ANSI control sequences.
- At the bottom, we’re just mapping the data descriptions (or table headers) to specific row positions.
There are actually many different signatures for a lot of the methods that TextGraphics and Lanterna generally provide. For details please read the official documentation.
Here’s the rest of the drawUI() method:
What we’re doing is first displaying the table headers to the right row and column positions, then drawing our double line box, printing some statistics and finally refreshing the screen to make our changes visible.
The Symbols class
We’re able to draw the double line box by using Lanterna’s Symbols class for passing special characters when invoking the setCharacter() method.
When messing with text graphics, the Symbols class is especially useful and here, on the left side, we have some symbols screenshot taken from Lanterna’s documentation. For the complete list, check out this page.
“ Code page 437 is the character set of the original IBM PC (personal computer), or DOS. …The set includes ASCII codes 32–126, extended codes for accented letters (diacritics), some Greek letters, icons, and line-drawing symbols.” — From Wikipedia, the free Encyclopedia
Displaying data and creating animated temperature bars
Moving on from the drawUI(), LanterminalEngine’s behavior involves one more method called displayData(). Here’s the first part:
Setting up the temperature bars consists of assigning block symbols to char variables for drawing them, setting border characters and calculating boundaries.
Next, we’re setting up the status indications on the right side of our GUI.
Mapping short temperature descriptions to specific ANSI TextColors and adding a KeyStroke object that Lanterna provides for user keyboard input detection. We’ve chosen F10 as a hotkey for quitting the program. For more source code on this simple hotkey functionality visit the GitHub repository.
Getting temperature data from the temperature monitoring system and placing them in the appropriate screen coordinates (remember, no pixels; just rows and columns)
At the bottom, we’re using Java’s DecimalFormat class; it’s the way to deal with the problem of shortening those lengthy double temperature values.
Finally, calculating the temperature bars lengths and drawing them.
Our custom text-GUI is now ready! Hopefully, you now have an idea how Lanterna works when using its Screen interface. Keep in mind that Lanterna is a huge and wonderful library. It has many different uses and can be a tool of choice in many different scenarios.
For the complete source-code, or if you have any suggestions/ideas and wish to join and collaborate please visit this project on GitHub.