Over the past couple issues, we've been working on our sample termcap program. Last issue, we discussed the basic required variables and functions for termcap programs, and their initialization sequence. This issue, we need to learn how to use termcap functions to control the terminal's cursor and how to read keyboard input from the terminal.
During the initialization sequence, we obtained all the terminal control strings that we needed to control the terminal's cursor. For instance, "CM" (cursor movement) points to the string of characters that will move the terminal's cursor to a new location. We need to refer to this variable whenever we wish to move the cursor.
Moving the cursor (or at least preparing the set of codes to allow the terminal to perform the task) actually requires a bit of processing, but luckily there is a library function to take care of this for us called tgoto(). By giving tgoto() our CM pointer and our desired cursor coordinates, it will return a pointer to a control string that can be sent to the terminal to reposition the cursor.
As it turns out, we always use the tputs() library function to send out control strings to our terminal. tputs() needs a pointer to a control string... Hey, that sounds familiar! That's exactly what tgoto() returns for our move-cursor operation, which means we can combine the tgoto() and tputs() calls together in one function! So, to reposition the terminal's cursor to coordinates (x, y) use:
tputs(tgoto(CM, x, y), 1, user_tputc);
You should notice there are two other parameters for the tputs() function ('1' and 'user_tputc'). The '1' is what's referred to as "lines_affected", and relates to our discussion of the "ospeed" variable in the last issue. On certain (older) terminals, a delay is needed to allow the terminal time to process special requests. The "lines_affected" parameter is used in calculating how long the delay should be. As you may recall, we set our ospeed variable to -1, which means no padding delay is necessary so the "lines_affected" we specify here really is of no consequence. The 'user_tputc' parameter is just a pointer to our user-defined function, whose task it is to output a single character to the terminal. Review the last issue for more information on this.
Looking at the tputs() call above, it really would be annoying to have to remember to type the '1' and 'user_tputc' parameters each and every time we move our cursor or do any special terminal operation in our program. Plus, what if we decide to rename our 'user_tputc' function? If we specified 'user_tputc' several times within our program, we'd have quite a few changes to make. So, you will notice in our termcap program we wrote another function named putpad() which only requires a string pointer be passed to it. This means in our program, we move the cursor to the coordinates (x, y) like this:
putpad(tgoto(CM, x, y));
To clear the screen:
If we had initialized a 'US' (underline start) variable, we could turn on underlining like this:
Now that's more like it! Easier to understand. For more examples on this, you can look in your Microware C manual under "Using the Termcap Library". You will note that 'CM' is the only variable that requires the use of another function (namely tgoto).
This may seem like a trivial task to the unsuspecting programmer. Why can't we just use the scanf() or read() functions to get data or characters typed in by the user? The reason is our program must appropriately respond to any arrow keys the user may press, including the backspace key. These keys may send more than one character back, depending on the terminal. On one terminal, the up-arrow key may send back only a one-byte code, whereas on a VT-100 terminal a 3-byte string may be sent. It is this variable-length keypad string that is the cause for complexity.
Unfortunately, there are no library functions to help us out. So, let's think about this for a moment. Whenever the user presses a "normal" key (the 'J' key, for example), we want to immediately process that keypress by displaying it on the user's screen. If the user presses a special key (arrow-key, home, or backspace), we want to immediately process the keypress by moving the cursor to an appropriate location. The problem is the terminal data will be coming in one byte at a time which means our input routine must be smart enough to realize if it is in the middle of processing a special key and know when to decide if a sequence of keys does not match any special key.
There is the interesting question of what to do if our program started to notice a special-key match, but turns out to be wrong. For instance, let's say the terminal we're using has it's up-arrow key defined to send the four characters "+$UP" (which would be very weird, but this helps get across the concept easier). Now, pretend you're the user working on some numbers and you start to type "+$100,000"... what is our program thinking after each character is pressed, and how should it respond?
When our program receives the "+" character, it notices that the user may have pressed the up-arrow key so no processing can yet take place. The "+" character must simply be placed into a buffer and the program waits for another character. Then our program receives the "$" character and remembers that it's in the middle of checking a special-key string. It still cannot determine whether or not the up-arrow key was pressed, so the "$" character is also buffered and the program waits again. Now, when the "1" is received, it is obvious to our program that the up-arrow key was not pressed. That means we can store the "1" character in our buffer, and then empty our buffer one character at a time to the user's screen. That is the important point here... when our program thinks it might be receiving a special-key sequence and it turns out to be wrong, we must then remember to send any buffered characters to the user's terminal (perhaps with the exception of unprintable characters).
I realize that sounds really tough (and maybe that's why there aren't a whole lot of termcap input programs out there), but the logic for "process_key()" isn't too bad:
if (EMPTYING BUFFER) remove next key from buffer display key else if (NO KEYS IN BUFFER) read key from terminal if key matches a special-key string process special key else if key partially matches special-key string buffer key else display keypress else if (KEYS IN BUFFER) buffer key if keys match a special-key string process special key destroy buffer else if buffer does not partially match special-key string turn on "EMPTYING BUFFER" flag
Looking at the "process_key" function in our sample termcap program, you should be able to match up the logic to the source code. As always, if you are having trouble please feel free to write to me.
You will notice when an arrow-key has been detected by the program, I have written it so the 'cx' and 'cy' variables (which I use to keep track of the current cursor position) are updated, and then later the cursor-move (CM) control string is send out to the terminal. For the backspace key, I blot out the character to the left of the cursor in addition to moving the cursor to the left.
Well, that's all for this issue. I sincerely hope the past few issues has provided a valuable tutorial on how to utilize the termcap C library and how to write termcap programs. Writing termcap programs which only do output isn't too tough. The hard part comes when you want to process input from the user, but now you have the routines written to take care of this for you!