summaryrefslogtreecommitdiff
path: root/content/clopher/03-TUI.md
diff options
context:
space:
mode:
Diffstat (limited to 'content/clopher/03-TUI.md')
-rw-r--r--content/clopher/03-TUI.md207
1 files changed, 207 insertions, 0 deletions
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<stdio.h>
+
+ 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<stdio.h>
+ #include<termios.h>
+
+ 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