Ekaitz's tech blog:
I make stuff at ElenQ Technology and I talk about it

TUI: A look to the deep

Read all the posts in Clopher series here.

This software have been introduced as a Gopher client but, as you can probably deduce from the previous post, the Gopher part is probably the simplest one. The complexity comes with user interaction. People are hard. That’s why we are going to delay that as much as possible, trying to cover all the points in the middle before we jump to the unknown.

Just joking. In fact, we have to shave many yaks before thinking about user interaction anyway. This text talks about them.

Are you talking to me?

Let’s remember we can classify programs by two different categories like this:

  • Non-interactive programs, often called scripts, are programs that take an input and return an output. There’s no interaction with the user in between. An example of this could be the command ls.

  • Interactive programs receive user input while they run and respond to the user while they are running. An example of this could be the machine that sells you the tickets for the subway, it asks you where are you going, then tells you the price, take your money and so on. All of this with the program constantly running.

Remembering what we talked about Gopher: it’s a stateless protocol. There’s no state stored in the server so all the queries must contain all the info related to them. Queries are independent.

This, somehow leaves the door open to two possible implementations of Clopher. The non-interactive one would work like curl. Getting the IP, port, selector string and an optional search string as input it would open the connection retrieve the result and return it.1

But Clopher is designed as an interactive program. More like lynx, where you interactively ask for the pages and have a local state that records your history and other things. This is a decision, it’s not imposed by the protocol.

Shellf boycott

There are some different ways to handle user interaction in TUI based programs. The simplest one is to read by line, waiting until the user hits ENTER to read the result. That’s the behaviour of the classic scanf function of C and many others like input in Python, etc.

In programs like Clopher, where the design is similar to lynx or vi, this kind of input makes no sense at all. The program needs to be able to capture every key pressed by the user and perform action in response to them. For instance, in vi when the user hits i in normal mode it needs to change to insert mode and when the user presses i in insert mode it needs to change the contents of the buffer.

The design of these kind of programs is simple to understand, it’s an infinite loop2 where key presses are captured and they change the state of the program. When the user hits the key combination that halts the program the loop is broken.

In simple C code the program would look like this:


main(int argc, char * argv[]){
    char c;
    // Create some state

        c = getchar();
        if( c == 'q'){ // Exit if user pressed `q`
            return 0;
        // Update state here
        putchar(c); // Show the character for debugging

Or the simplified Clojure equivalent:

(loop [c     (char (.read *in*))
       state (->state)] ; Create some state

  (when (not= c \q)     ; Exit if user pressed `q`
    (print c)           ; Show the character for debugging
    (recur (char (.read *in*))
           (update-state c state))) ; Update state

Looks simple, right?

Wait a second, there’s a lot of stuff going on under the hood here. If you run the code in any POSIX compatible operating system (I didn’t test on others, and I won’t) you’ll find the code might not be doing what we expected it to: The getchar (or .read) calls will wait until ENTER is pressed in the input buffer and then they’ll get the characters one by one. But we want to get them as they come!

Saints and demons — canonical mode

In POSIX operating systems, the input is buffered by default. But that behavior can be configured following the POSIX terminal interface under the name canonical mode or non-canonical mode. The mode we are looking for is the non canonical mode. You can read more about it in the Wikipedia.

Choosing the non-canonical mode has some extra options: one controls the number of minimum characters to have in the buffer to perform a read operation and the other defines the amount of tenths of second to wait for that input3. Choosing the right value for those fields (c_cc[MIN] and c_cc[TIME]) depends on the kind of interaction we are looking for.

Make Dikembe smile — blocking

Setting c_cc[TIME] field to 0 means the read operation will wait indefinitely until the minimum amount of characters defined with c_cc[MIN] are waiting in the buffer. Together with that, the c_cc[MIN] can be 0 that means the read operations will wait until there are 0 characters in the buffer, or, in other words, they won’t wait.

Be aware that both fields can provoke the read operations in the input buffer be non-blocking operations and that will cause the read operation to return with no value.

In the case of Clopher, I decided to set the c_cc[MIN] to 1 so the read operations block until there’s at least one character in the buffer (that means they will always return something) and the c_cc[TIME] to 0 so the read operations have no timeout and will block until a character arrives.

Depending on the application you are developing, you might choose other kind of blocking configuration. For instance, setting a timeout can let you process other parts of the system and wait for the input in the same thread.

We’re talking about practice? — termios

So now we know where to find this theoretical configuration it’s time to put it in practice. In POSIX the standard way to access this is via termios4. It has some details that are not specified and depend on the implementation, so it might have some differences from Linux to BSD or whatever.

tcsetattr and tcgetattr calls can be used to set and read the terminal configuration via termios. Check this example, compile it and compare it with the C code of the previous example:


main(int argc, char* argv[]){
    // Get interface configuration to reset it later
    struct termios term_old;
    tcgetattr(0, &term_old);

    // Get interface configuration to edit
    struct termios term;
    tcgetattr(0, &term);

    // Set the new configuration
    term.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
    term.c_cc[VMIN]  = 1;   // Wait until 1 character is in buffer
    term.c_cc[VTIME] = 0;   // Wait indifinitely
    //TCSANOW makes the change occur immediately
    tcsetattr(0, TCSANOW, &term);

    char ch;
        if(ch == 'q'){
            // Set old configuration again and exit.
            // If it's not set back the normal configuration of the
            // terminal will be broken later!
            tcsetattr(0, TCSANOW, &term_old);
            return 0;
        ch = getchar();

All the code has enough comments to be understood but there are some weird flags it’s better to check in termios documentation.4

But this is C code and Clopher is written in Clojure!

I know but this is becoming long and boring. Why not wait until I get some spare time and write the next chapter? You have tons of information to check until I write it so you won’t be bored if you don’t want to.

See you next.

  1. In fact, you can navigate the Gopherverse like this with curl

  2. Unsurprisingly called main loop. Programmers are very creative. 

  3. That read operation is what getchar is doing under the hood. 

  4. man termios or visit online man pages