OS-9/OSK Answers! by Joel Mathew Hegberg
Tackling TERMCAP, Part II


In the last issue, I gave the source code for an entire program which uses the TERMCAP (TERMinal CAPabilities) library to control the cursor movement and decode input from the user. The program simply clears the screen and allows the user to type anywhere on the screen, using the arrow keys to move the cursor. Now, it's time to explain how the program works! I will assume you have the previous issue on hand to refer back to while I explain, and I have to assume that those reading this understand the concept of pointers to (i.e. addresses of) data.

I like to break the TERMCAP learning process down into four categories -- required variables and functions, initialization, outputting data, and inputting data. Not every program will have both input and output via termcap, but all termcap programs must have an initialization sequence and the required variables and functions. For instance, your program may just display fancy character patterns on the user's screen for entertainment (a screen-saver, perhaps). This would not require and termcap-supported input functions, so those could be omitted.

Required Variables and Functions

The first thing you need in your termcap program is '#include <termcap.h>' so all the library functions will work! Next, we need a couple variables to remember how many lines and columns are available on the user's terminal, and we also need to define a variable called "ospeed", which I'll explain a little later. These three variables can be defined as type 'short' (2-byte integers) like this:

short lines, columns, ospeed;

At this point, we need to decide what terminal capabilities our program needs in order to do what we want it to do. (Remember, a list of terminal capabilities is found on pages 8-31 through 8-37 in the "Using Professional OS-9" manual.) For each capability, our program will need to have a character pointer assigned to use it. For example, our program will need to have "cl" (CLear screen), "cm" (Cursor Move), "ku" (Keypad Up), "kd" (Keypad Down), "kl" (Keypad Left), "kr" (Keypad Right), and "kb" ( Keypad Backspace). The naming convention used under TERMCAP is simply using the two-character capability name and capitalizing them. So, we define our capability string pointers like this:

char *CL,
      *CM,
      *KU,
      *KD,
      *KL,
      *KR,
      *KB;

In addition, the termcap library routines (tgoto in particular) require three more variables be defined -- PC_ (for Pad Character... note this is not a pointer), *UP (move cursor UP a line), and *BC (for Backspace Character string). You will note they are in the program (see the previous issue).

So now we've just set up some pointer variables... the question you should be asking is, "What do they point to?" I'm glad you asked! They point to character strings stored in a termcap buffer, that is yet another required variable definition. Our initialization routine will fill this buffer with all the character strings our program needs to control the user's terminal and point our pointer variables to those strings. So, how big should our buffer be? The examples I've seen have it set to 400 bytes, but you may want to change this if you have problems so it's best to make it a #define:

#define TCAPSLEN 400
char tcapbuf[TCAPSLEN];

Alright, that's all for required variables. Now, we just need to write one function which is required by the termcap library called user_tputc(). The exact name isn't important, so feel free to rename it. It outputs a single character (passed to it as a parameter) to the user's terminal. Here's what you'll find in the program we're working on:

/* writes one character to terminal.   */
/* needed by tputs() library function. */
int user_tputc(c)
char c;
{
   return (write(STDOUT, &c, 1));
}

Initialization

The initialization of our termcap program is a pretty straight-forward process. We need to find out what type of terminal the user is on, read the information about the terminal from the /dd/SYS/termcap file (or from the environment variable TERMCAP, if defined), extract only the capabilities we want and store them in our termcap buffer (tcapbuf) while pointing our string pointers to them, and find out how many lines and columns are on the user's terminal.

We also have to do quite a bit of error checking as well. We need to handle conditions where the user's TERM environment variable may not be defined, the terminal type is unknown, not all of our needed terminal capabilities are defined in the termcap file, and our termcap buffer (tcapbuf) is too small.

We now begin learning about the termcap library functions. There are four functions used for initialization -- tgetent(), tgetnum(), tgetflag(), and tgetstr(). The first function, tgetent(), grabs the raw termcap entry and stores it in a temporary buffer. It is important to realize that this is a raw form of the termcap entry that is unusable for input/output. We want to extract the termcap strings our program needs and store them in a usable form in our termcap buffer (tcapbuf). (My manuals recommend having the temporary buffer at least 1024 bytes or longer to accommodate large termcap entries.) So for our initialization routine, let's define a temporary buffer (tcbuf), a pointer for the user's terminal type (*term_type), and a couple generic pointers (*temp and *ptr):

char tcbuf[1024], *term_type, *temp, *ptr;

Looking back at the previous issue, we can see how term_type = getenv("TERM") is used to find out the user's terminal type (or if one isn't defined, in which case an error is printed). We use this term_type pointer in our tgetent() call to grab the raw termcap entry and detect if the terminal type is unknown. The tgetent() function is smart enough to determine if the user has defined a TERMCAP environment variable to hold their termcap entry.

if (tgetent(tcbuf, term_type) <= 0)
{
    fprintf(stderr, "Unknown terminal type '%s'.\n", term_type);
    exit(1);
}

Now we start filling up our termcap buffer (tcapbuf) with the capability strings that we need, and set up our pointers to them. While filling tcapbuf, we have to keep track of where the next string needs to go in tcapbuf, so we'll use our generic pointer 'ptr' for this task. tgetstr() automatically adjusts our pointer for us and returns a pointer to the location of the string stored in our buffer. This makes extracting our needed capabilities very easy:

   /* read the termcap entry */
   ptr = tcapbuf;
   if (temp = tgetstr("PC", &ptr)) PC_ = *temp;
   CL = tgetstr("cl", &ptr);
   CM = tgetstr("cm", &ptr);
   KU = tgetstr("ku", &ptr);
   KD = tgetstr("kd", &ptr);
   KL = tgetstr("kl", &ptr);
   KR = tgetstr("kr", &ptr);
   KB = tgetstr("kb", &ptr);
   UP = tgetstr("up", &ptr);

If any of our needed terminal capabilities do not exist, their pointers are set to NULL. We can then check to make sure we have everything we need by looking at our pointers and making sure all of them are not NULL:

   /* make sure we have everything */
   if (!(CL && CM && KU && KD && KL && KR && KB && UP))
   {
      fprintf(stderr, "Incomplete termcap entry.\n");
      exit(1);
   }

To discover the number of lines and columns the user has on his terminal, we use tgetnum(), since we're getting a number rather than a string. tgetnum() does not touch our tcapbuf. This is the same for tgetflag(), which returns 1 for true or 0 for false for a boolean termcap capability. Only tgetstr() updates tcapbuf and our generic pointer:

   lines = tgetnum("li");
   columns = tgetnum("co");
   ospeed = -1;  /* no padding */
   
   if (lines < 1 || columns < 1)
   {
      fprintf(stderr, "No rows or columns!\n");
      exit(1);
   }

What the heck is that 'ospeed' variable? Let me try to explain. Some older terminals required a delay after each special operation (like moving the cursor, clearing the screen, etc.) before more data could be sent. The delays are in the form of "pad characters" which are sent out for a certain duration of time. How long are the pad characters sent to the terminal? That's what ospeed determines. ospeed stands for Operating SPEED of the terminal, and represents the baud rate. Under OS-9, advanced programmers will recall that baud rates are given values 0 through 16 (5=300, 7=1200, 10=2400, 14=9600, etc.). You can obtain the value for the user's terminal by making a call to _gs_opt() and looking at _sgs_baud. Or, you can do what I did, and make the assumption that the user is using a relatively modern terminal that doesn't require padding and set ospeed to -1. With ospeed set to -1, no padding is sent to the users terminal during special operations.

Finally, we just need to check to make sure our generic pointer hasn't gone past the end of our terminal capabilities buffer (tcapbuf)! This is an awkward error, since if it has gone past the buffer, there's no telling what it could have overwritten in our program. The best thing is just get out quickly.

   if (ptr >= &tcapbuf[TCAPSLEN])
   {
      fprintf(stderr, "Terminal description too big!\n");
      exit(1);
   }

This takes care of initialization! Our tcapbuf now contains all our needed terminal capability strings and our pointers are initialized. In the next issue, I'll explain how to do I/O using our termcap example program, so be sure to keep the all your 68'Micros issues on hand!


Boisy Pitre recently sent me some source code to help K-Windows programmers determine if the user is, in fact, running on a K-Windows terminal. He also included another routine to return the edition number of K-Windows on the system. The latter routine will be of interest to non K-Windows as well, since it can (with few changes) work for any module in memory. My thanks to Boisy for allowing me to reprint this code here!

#include <stdio.h>

main()
{
    if (isKWindowsDevice(1) == 1) {
        printf("stdout is a KWindows device!\n");
    } else {
        printf("stdout is NOT a KWindows device!\n");
    }
    printf("Windio #%d\n", KWindowsEdition());
}

/*
 * this function returns 1 if the current path is open
 * under a K-Windows device, else it returns 0
 *
 * Boisy G. Pitre -- 12/8/94
 */

#include <module.h>

isKWindowsDevice(path)
int path;
{
    char device[32], driver[8];
    mod_dev *descAddr;
    mod_dev *modlink();

    /* get the name of the path's device */
    if (_gs_devn(path, device) == -1) {
        /* failed to get device */
        return(0);
    }

    /* attempt to link to path's device ONLY if it's a descriptor */
    if ((descAddr = modlink(device, mktypelang(MT_DEVDESC, ML_ANY))) == -1) {
        /* failed to link to path's device */
        return(0);
    }

    munlink(descAddr);      /* unlink to be nice */

    /* copy driver string and check to see if it's windio */
    strncpy(driver, (int)descAddr + descAddr->_mpdev, 6);
    driver[6] = '\0';
    if (strucmp(driver, "WINDIO") != 0) {
        return(0);
    }

    /* it's more than likely a K-Windows window */
    return(1);
}

int strucmp (str1, str2)
register char *str1, *str2;
{
     register char *p;
     char *index();

     if ((p = index (str1,'\n')) != NULL)   *p ='\0';
     if ((p = index (str2,'\n')) != NULL)   *p ='\0';

     while (toupper (*str1) == toupper (*str2) && *str1 && *str2) {
      ++str1;
      ++str2;
     }

     if (*str1 == *str2)  return(0);
     if (*str1 > *str2)   return(1);
     return(-1);
}

/*
 * this function returns the edition number of windio
 * or -1 if the windio driver cannot be found
 *
 * Boisy G. Pitre -- 12/8/94
 */

int KWindowsEdition()
{
    struct modhcom *windioDrv;
    struct modhcom *modlink();

    /* attempt to link to windio driver */
    if ((windioDrv = modlink("windio", mktypelang(MT_DEVDRVR, ML_ANY))) == -1) {
        /* failed to link to windio driver */
        return(-1);
    }

    munlink("windio");      /* unlink to be nice */

    return((int)windioDrv->_medit);
}

* THE END *