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:
#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:
(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 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:
#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.