From b25ba3823300e6aefe633e13059b6d36f7306a07 Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Mon, 13 Jan 2020 15:27:36 +0100 Subject: Update content --- content/clopher/03-TUI.md | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 content/clopher/03-TUI.md (limited to 'content/clopher/03-TUI.md') diff --git a/content/clopher/03-TUI.md b/content/clopher/03-TUI.md new file mode 100644 index 0000000..fb5ad02 --- /dev/null +++ b/content/clopher/03-TUI.md @@ -0,0 +1,207 @@ +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 -- cgit v1.2.3