Title: TUI: A look to the deep Date: 2019-05-30 Categories: Series Tags: Clopher - TUI Gopher client Slug: clopher03 Lang: en Summary: Research and work on the Terminal based User Interface of Clopher, the gopher client. > Read all the posts in [Clopher series here]({tag}clopher-tui-gopher-client). 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. ### Shell*f 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 loop[^2] 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: ::clike #include int main(int argc, char * argv[]){ char c; // Create some state while(1){ 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: ::clojure (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][canon]. 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 input[^3]. 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 `termios`[^4]. 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: ::clike #include #include int 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; while(1){ 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(); putchar(ch); } } 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](https://linux.die.net/man/3/termios) [canon]: https://en.wikipedia.org/wiki/POSIX_terminal_interface#Input_processing