Jon Baker, Graphics Programming

    home

    writings

Terminal From Scratch

Motivation

  I've been using Omar Cornut's dearImGUI for a couple years now. It serves a lot of purposes and is quite honestly a pretty full featured UI framework. I've been impressed with what's possible to implement with it, and how simple the code involved is. But for a while, I've been interested in Quake-style terminals as an alternative method of input. On Ubuntu, I use a piece of software called guake, which gives you a terminal that can be brought up and instantly typed into with the system-wide hotkey, f12. I find this is often very convenient, to avoid having to use the mouse to navigate somewhere to open up a terminal.

  More recently, honestly for quite some time now, I have been a little bit upset with SDL's input event system. I have had mixed results, but one of the big limitations I had was that when I was holding multiple cursor keys I was not getting the events very consistently. For quite a while, I have adapted by using SDL_GetKeyboardState, and indexing into that with SDL keyboard scancodes to get the instantaneous state of the keys. This works, but does not have some of the features that you would want from an event system. Being the instantaneous values, they don't carry any information about when they entered that state, if that was last frame or an hour ago. This is not a good situation, when it is difficult (basically impossible) to press and realease a key in a way that does not show "active" for several frames at a time. I press the key, intending to see one activation, but because it's active for say, 5 frames, I'm seeing it 5 times. So I decided to write my own input layer, using this instantaneous state from SDL_GetKeyboardState.

Input Layer

  The basic state representation is in the bits of 4 uint32's. This gives 128 bits of state, of which about 110 are being used for alphanumerics, punctuation, various control keys, and the mouse buttons. Because this state is so small, I actually keep a ring buffer of N of these, so that I have some history to look at when making determinations about the state of the keys. One of the most critical uses here is the use of the combination of last frame's state and this frame's state to determine rising and falling edge events. A rising edge refers to the transition from off to on, and the falling edge refers to the opposite, when it goes from on back to off. Looking at the 2 samples, we now have four cases:

  And this four-state input is sufficient to implement an event system. Instead of doing something event-queue-based, I've got it set up so that I'm interfacing with it pretty similarly to the raw array from SDL_GetKeyboardState. But instead of just being a bool, each key enum passed to the overloaded square brackets can be used in a couple different modes: boolean on/off or this 4-state input are the primary usage. Another one that I put in and haven't really had chance to try yet, you can look at the ratio of how many bit flags show "on" across the N frames in the history, and return a float "soft activation". I think it might be interesting for some stuff like inertia while doing navigation, I am thinking about places it might be useful.

  Additionally, I can look at bitmasks for text input. Because it's all bits, I can just look at ranges of the bits in the uint32 state to determine, for example, "are there any alphanumerics pressed?". This is nice, and means you don't have to iterate through the whole list every frame. The terminal uses the rising edges of the alphanumerics and punctuation keys for text input. It's been very consistent and matches very closely to user expectation for "typing", for text input on the command line.

Display

  A while ago, I implemented this text renderer in jbDE - for the most part, it's just been serving as a frametime display in the bottom right hand corner. I have long had aspirations for it to become more than that, since it supports multiple layers and full 8-bit RGB per glyph. This terminal uses three layers: an optional background layer, a foreground layer, and a highlight layer which is mostly used for the blinking cursor on the command line. You can see below, an example - this is the formatted report from my texture manager, which gives a lot of information about the currently active textures and binding, etc. management utilities.

perlin2

  Something I added for this, because I didn't really have a very good utility for generating strings with distinct color per glyph, was the cChar builder class. This was just a little experiment with the builder pattern, where I could chain function calls and do something like builder().append( "blah", color1 ).append( "blah", color2 ).flush(), where flush() would return the vector of cChar types. This is just a struct of uint8 glyph ID plus 3 uint8 values indicating color. Some nice utilities built into that class for automatically adding a timestamp, handling default colors and some indexed color palette stuff.

Commands and Cvars

  There are two important entities in the interactive portion of the terminal. You have console comands which "do something", potentially taking arguments, and cvars, which is a global system of console variables.

  The Cvar system defines a set of types - the basic types are booleans, integers, floats, and two to four channel integer and float vectors. There are also strings, which right now have some limitations that they basically can't contain whitespace because of how the stream operator reads strings. As an example of how these cvars are interacted with, there is a console command assign, which is overloaded for each type, and can put a variable from the command line to a matching valued cvar.

  The commands are somewhat more nebulous, because of their flexibility. This is an example, where we can see the components that make up a command declaration:

perlin2

  There are a couple of fields here, it's setting up a basic test command. The first argument is the constructor for a vector of aliases for this command. There has to be at least one string that identifies this command, so that it can be used - if you want to setup shorter aliases, it's easy to do that here. Following that, we have a list of argument declarations: this matches the structure of the cvars, and defines the expected sequence of input arguments and their types. Following that is the lambda containing the meat of the command itself, and finally a string giving a brief description of the command.

  Almost everything in this system uses some kind of string indexing, and has an attached string label indicating its purpose... which is nice for error reporting, and not too big a limitation because this layer is not super performance critical. Incidentally, because of how arguments are strictly parsed, this also has good support for overloading of functions (ex. the assign command). As you can see below, when the test command is called with no arguments, the prompt reports a parsing failure - when the arguments match, no such error is generated. If the command string matches the alias for an existing command, report the signature; otherwise, report "command not found".

perlin2

  And that's really all there is to it. This system has yet to be tested very much in real usage, but one area where I think it would be useful is for things like Daedalus, managing geometry resources that are getting passed to the GPU. That's things like the masked planes textures, or the explicit primitives list. This could be commands like addXXX or commands to dump the current state to the command line. I'm going to do some dogfooding with it the next couple months, and see if it makes sense for the way I want to use it. I have come up with some classifications of the different types of commands represented in the engine:

Interaction and Quality of Life

  There's a lot of little stuff that you take for granted when you use most system terminals. I spent a little bit of time setting up tings like input history navigation, where you are able to hit the cursor up and down keys to go through the list of input strings that have been given since the initialization of the engine. I did tab completion, where it finds the last token in the input command line, and uses this code I found to match the longest substring in the list of active commands and cvars. If there is only one that matches, use that string... otherwise, list up to 10 matching strings followed by "...".

  The terminal isn't intended to be shown at all times. Similar to guake, I've got an active toggle bound to one of the F keys, where I can show/hide it whenever I want. When it is hidden, it does not add text to the command line or accept any other key input, it just waits till it sees the active toggle to become active again. This makes it practical to pull up, type a command into, and then hide it. I think this is a nice interface, and it seems like something I could get used to.

  Another thing - when parsing arguments to a command - you can pass literals, as I showed. But, you can also pass cvars as arguments to commands. If they are of a matching type for the argument that is being parsed, it is considered valid input and accepted as an argument to the command. I think this is a really nice thing to have, lends itself to building this out into something like a scripting interface for the engine. It could already start moving that way, by processing lines of input sequentially... though there is not yet any form of control flow besides quit.

Future Directions

  I think this is already becoming an interesting alternative method of input. It's got a lot of potential for user interaction with my projects. I'm excited about the potential for building out a set of system cvars, which would give global access to things like screen width, height, etc. Because of the flexibility afforded by the lambdas on the commands, they can be used for arbitrary functionality across many different scopes within the engine. This is a significant addition to be able to start building out some base functionality to make this feel a bit more like "a real engine". I'm not really interested in making a general purpose "game engine", this is just in support of my various projects. I like that it's been able to specialize and become pretty much exactly what I want it to be, over the past few years. Eventually, I'd like to move from OpenGL to Vulkan, and a lot of this can be brought along.


Last updated 9/2/2024