summaryrefslogtreecommitdiff
path: root/content/clopher/03-TUI.md
blob: fb5ad0201d9763ddcf109595cc3e4f42c0f62669 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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