Part 8. Console programming

The nonportability of conio

Writing plain text command-line utilities are great, but eventually you may want to move on from programs that only scroll up from the bottom of the screen. If you want to access the screen directly, including colors and dialog boxes, you need a new library called conio.

The conio library performs console-level input and output. Unfortunately, conio is not very portable. Every compiler's implementation of conio is a little different, so you need to be careful. For example, while the getch function to get a single character from the keyboard (similar to getchar) exists across many conio implementations, other functions that clear the screen or move the cursor around differ.

Let's start with a simple conio example. The FreeDOS PAUSE command displays a prompt, then waits for the user to press a key. We wrote a simple version of PAUSE in part 3 using scanf and in part 4 using getchar. But in both cases, the user had to press Enter before PAUSE could continue.

By reading the keyboard directly, we can immediately get a keystroke from the keyboard. Let's re-write PAUSE using conio:

#include <stdio.h>
#include <conio.h>

int
main()
{
   printf("Press any key to continue ...");
   fflush(stdout);
   getch();

   return 0;
}

This version of PAUSE is very short: display a prompt using printf and wait for the user to press a key on the keyboard.

Normally, using getchar or other standard C library input/output functions will cause any printed text to completely display on the screen (this is called flushing the output stream

). But conio functions are direct calls to hardware, and bypass the usual flushing mechanisms. That means the prompt may not display to the user until we manually flush the output stream. I used fflush to do that.

Clearing the screen

The conio library provides direct access to the display, so the conio library includes lots of functions that manipulate the screen. Programs often need to clear the screen, so there's a conio function for that. The _clearscreen function will clear the screen with the current background color. The OpenWatcom _clearscreen function is defined in <graph.h> and takes these arguments:

_GCLEARSCREENClear the entire screen
_GVIEWPORTClear the graphics view port or clip region
_GWINDOWClear the current text window

With this, it's almost trivial to write our own version of the FreeDOS CLS command. CLS clears the screen and sets the cursor to the top of the screen, which is the same behavior of _clearscreen.

#include <graph.h>

int
main()
{
   _clearscreen(_GCLEARSCREEN);
   return 0;
}

This CLS program doesn't print anything to the screen, other than using the _clearscreen function. Since we only use the one function, we only need the graph.h header file. We don't need to include stdio.h like we do in other programs.

Moving text around the screen

Writing your own full-screen applications will require placing text at specific locations on the screen. So let's explore a few functions to do that.

struct rccoord _settextposition(short row, short col)Sets the current screen position to start writing text. Think of the screen position as a 𝓎,𝓍 coordinate. Returns the previous screen position before calling _settextposition.
struct rccoord _gettextposition()Returns the current screen position. The return value is stored in a structure with elements row and col for the 𝓎,𝓍 coordinate.

But before we call either of those functions, you should first set the screen mode appropriately. The conio function _setvideomode supports several direct screen modes, including:

_DEFAULTMODEThe default text mode, the original text mode before running your program.
_TEXTBW40Black and white text on a 25 row × 40 column screen. Typically used in original IBM monochrome displays and Hercules mono displays, but also available in other display systems.
_TEXTBW80Black and white text on a 25 row × 80 column screen. Commonly used in original IBM monochrome displays and Hercules mono displays, but available on other display systems too.
_TEXTMONOBlack and white text on a 25 row × 80 column screen. Commonly used in original IBM monochrome displays and Hercules mono displays, but available on other display systems too.
_TEXTC40Color text on a 25 row × 40 column screen. Usually for CGA displays, but also available in later display systems.
_TEXTC80Color text on a 25 row × 80 column screen. This should be safe to use on modern systems.

_setvideomode returns a short value that indicates the number of rows available in the selected screen mode, or zero if an error.

short _setvideomode(short mode) Sets the screen mode

If you don't want to make assumptions about the display, you can also use the _getvideoconfig function to read the display's capabilities. This returns a pointer of type struct videoconfig that is the same returned in the parameter list:

struct videoconfig *_getvideoconfig(struct videoconfig *cfg) Returns information about the display's capabilities.

The struct videoconfig structure contains a list of elements, including these for text-mode:

numtextrowsNumber of text rows (for example, 25)
numtextcolsNumber of text columns (usually 80 or 40)
numcolorsNumber of actual colors
modeThe current video mode
adapterThe video adapter connected to the system
monitorThe display or monitor connected to the system

The adapter field contains one of these values:

_MDPAThe original IBM monochrome display/printer adapter
_HERCULESHercules monochrome adapter
_CGAThe IBM color/graphics adapter
_MCGAThe multi-color graphics array
_EGAThe enhanced graphics adapter
_VGAThe standard video graphics array
_SVGAA compatible SuperVGA display adapter

The monitor field contains one of these values:

_MONOA regular monochrome display
_ANALOGMONOAn analog monochrome display
_COLORA regular color display
_ANALOGCOLORAn analog color display
_ENHANCEDAn enhanced color display

Let's demonstrate a few of the text display functions with a simple demonstration program:

#include <conio.h>
#include <graph.h>

int
main()
{
   struct rccoord rowcol;

   _setvideomode(_TEXTC80);
   rowcol = _gettextposition();

   _settextposition(10, 40);
   _outtext("Hello world");

   _settextposition(rowcol.row, rowcol.col);

   getch();
   _setvideomode(_DEFAULTMODE);

   return 0;
}

screenshot of Hello World program

Let's look more closely at a few key parts of that program. First, we set the screen mode to a color 80-column display with _setvideomode, then read the current cursor position with _gettextposition. But the _setvideomode function also sets the cursor location to the top-left corner of the screen, so it turns out we don't need the _gettextposition function here.

When setting the new screen position, the _settextposition function uses 𝓎,𝓍 coordinates, so this is actually on row 10 at column 40.

Why text only has sixteen colors

Before we move on to text color, let's first discuss why DOS has the color palette it does. PCs can display text in sixteen colors and eight background colors. You may wonder why only these colors, and why sixteen text colors and only eight background colors. To understand that, we need to take a brief detour into the first IBM Color/Graphics Adapter, or CGA. The DOS color palette originates from that standard.

In CGA, every pixel on your screen is actually a mix of colored lights, implemented as phosphor elements on the CGA display. When mixing light colors, you only need three colors to make any other color: red, green, and blue. If you mix them all at equal intensity (all on), you get white. Total absence of any of these lights (all off) is black. In between, you can mix the red, green, and blue light to get other colors.

In the simplest form, consider a computer display that can only display red, green, and blue light elements that were either on or off. We can represent these light color combinations using binary values, where 1 represents on, and 0 is off.

RGBColor
000Black
001Blue
010Green
011Cyan
100Red
101Magenta
110Yellow
111White

That gives eight colors, from RGB=000 to RGB=111. To double the number of available colors, you might add an extra bit as iRGB to represent the intensity. If the intensity bit is 1, we get full brightness for any color that's displayed. If the intensity bit is 0, we might pick some mid-level brightness.

iRGBColor
0000Black
0001Blue
0010Green
0011Cyan
0100Red
0101Magenta
0110Yellow
0111White
1000Bright Black
1001Bright Blue
1010Bright Green
1011Bright Cyan
1100Bright Red
1101Bright Magenta
1110Bright Yellow
1111Bright White

The problem with that simple approach is that Black and Bright Black are the same value. Both iRGB=0000 and iRGB=1000 are RGB=000, which is the same value of black.

This is the problem that IBM faced when designing the first IBM Color/Graphics Adapter, or CGA. To display the full sixteen colors, IBM actually implemented a modified iRGB encoding, using two intermediate values, at about one-third and two-thirds intensity. Most "normal" mode (0–7) colors used values at the two-thirds brightness. with the intensity bit set to 0, any 1's in the RGB values are displayed at two-thirds brightness—with the exception of yellow, which was assigned a one-third green value that turned the color brown. With the intensity bit set to 1, any 0's in the RGB values are displayed at one-third brightness, and any 1's are displayed at full brightness.

iRGBColor
0000Black
0001Blue
0010Green
0011Cyan
0100Red
0101Magenta
0110Brown
0111White
1000Bright Black
1001Bright Blue
1010Bright Green
1011Bright Cyan
1100Bright Red
1101Bright Magenta
1110Bright Yellow
1111Bright White

And that's how DOS got sixteen text colors! That's also why you'll sometimes find "Brown" labeled "Yellow" in some references, because it started out as plain yellow before the intensifier bit. Similarly, you may also see "Bright Black" listed as "Gray."

You may wonder why only eight background colors? Note that DOS also supported a "Blink" attribute. With this attribute set, your text could blink on and off. The "Blink" bit was encoded at the end of the foreground and background bit-pattern:

Bbbbffff

That's a full byte! Counting from right to left: four bits to represent the text foreground color (iRGB=0000 Black to iRGB=1111 Bright White), three bits to code the background color (RGB=000 Black to RGB=111 White) and one bit for the Blink attribute.

Displaying text in color

With the background information about how PCs display color, we can apply the different iRGB values to display text in color. Using the range 0 to 15 for the text colors, and 0 to 7 for the background colors, you can use the _settextcolor and _setbkcolor functions to set the text and background text colors:

short _settextcolor(short fg)Set the text color, 0 to 15. You can generate blinking text by adding 16 to the fg color.
long _setbkcolor(long bg)Set the background color, 0 to 7. This is a long value because the same function may also be used to set background color in graphics mode, which uses a larger color range.

…where the colors are:

#Color #Color
0Black 8Bright Black
1Blue 9Bright Blue
2Green 10Bright Green
3Cyan 11Bright Cyan
4Red 12Bright Red
5Magenta 13Bright Magenta
6Brown 14Bright Yellow
7White 15Bright White

For example, to set bright white text on a blue background, you would use this code:

   _settextcolor(15);
   _setbkcolor(1);

A neat trick is to read arguments from the command line to set the DOS colors. To write this program, let's start with a function that compares a string to each color name, and returns the appropriate numeric value:

#include <stdio.h>
#include <graph.h>
#include <string.h>

int
strcolor(char *color, int allowhigh)
{
   /* a reminder on colors
      RGB = Red Green Blue
      000 = black
      001 = blue
      010 = green
      011 = cyan
      100 = red
      101 = magenta
      110 = brown(yellow)
      111 = white
    */

   /* but PCs actually use iRGB to set a brightness.
      set i=1 for bright colors */

   char *colornames[] = { "black", "blue", "green", "cyan", "red", "magenta",
      "brown", "white",
      "brightblack", "brightblue", "brightgreen", "brightcyan", "brightred",
      "brightmagenta", "brightyellow", "brightwhite"
   };
   int colornum;
   int ncolors;

   /* loop and compare */

   if (allowhigh) {
      ncolors = 16;
   }
   else {
      ncolors = 8;
   }

   for (colornum = 0; colornum < ncolors; colornum++) {
      if (strcasecmp(color, colornames[colornum]) == 0) {
         return colornum;
      }
   }

   /* no match, return default */

   if (allowhigh) {
      return 7;
   }

   /* else */
   return 0;
}

int
main(int argc, char **argv)
{
   int fg, bg;

   /* check command line */

   /* usage: color fg bg */

   if (argc != 3) {
      fprintf(stderr, "Incorrect command line arguments\n");
      fprintf(stderr, "usage: COLOR fg bg\n");
      fprintf(stderr, "\nfg and bg can be:\n");
      fprintf(stderr,
              "\tblack, blue, green, cyan, red, magenta, brown, white\n");
      fprintf(stderr, "\nfg can also be:\n");
      fprintf(stderr,
              "\tbrightblack, brightblue, brightgreen, brightcyan, brightred, brightmagenta, brightyellow, brightwhite\n");
      return 1;
   }

   /* pick fg & bg */

   fg = strcolor(argv[1], 1);
   bg = strcolor(argv[2], 0);

   _settextcolor((short) fg);
   _setbkcolor((long) bg);

   _clearscreen(_GCLEARSCREEN);

   printf("<%s> <%s> is %d %d\n", argv[1], argv[2], fg, bg);

   return 0;
}

This sample program sets the text color and background colors to the user's wishes, then clears the screen. The _clearscreen function uses the colors that were set by _settextcolor and _setbkcolor as it clears the screen.

Drawing text windows

Since conio provides direct access to the screen, it's possible to use that control to create text-mode displays. This concept is sometimes referred to as a TUI, or Text User Interface (instead of GUI, or Graphical User Interface, on modern systems). You can create a TUI using text windows.

However, don't confuse a "text window" with a "window" on a modern operating system like Mac or Windows. In text-mode displays, a "window" simply refers to a region on the text display. You can define a region from a top-left 𝓎,𝓍 to a bottom-right 𝓎,𝓍, and manage text within that region.

For example, the _clearscreen function with the _GWINDOW argument will clear only the current text window. With _setbkcolor, you can quickly change the background color of this text window. This lets you create your own TUI, by creating text windows with different colored backgrounds to define a screen title, main program area, or a pop-up alert.

Let's explore this with a sample program. Suppose you wanted to write a card game. You could create a tableau as your play field, and place cards on it.

To write this sample program, let's start with a few functions that create the individual Text User Interface elements. The clear_tableau function simply clears the entire screen with a green background. Similarly, the clear_message function clears the bottom line of the screen with a cyan message area. The print_message function uses clear_message to display information to the user in this message area.

Having cleared the tableau, you can draw cards with the draw_card function. For this demonstration program, I'll skip over a few details that you might include in a full program, such as code to place your cards appropriately, or to define the suit and value of each card. Instead, this draw_card function will display a single pre-defined card: the 4 of Hearts. But I'll leave some definitions so you can see how to expand this on your own.

With a little creativity, you can create the illusion of a drop shadow on a pop-up alert by first defining a black text window that's the same size as your pop-up, but offset by one row and one column. The print_messagebox function does just that before printing the message.

#include <conio.h>
#include <graph.h>

void
clear_tableau(void)
{
   /* draw a tableau */

   _settextwindow(1, 1, 25, 80);

   _setbkcolor(2);                     /* green */
   _clearscreen(_GWINDOW);

}

void
draw_card(int cardnum, int suit, int value)
{
   /* draw a playing card using a text window */

   _settextwindow(5, 10, 9, 16);       /* card is 5..9 rows (5) and 10..16 cols (7) */

   _setbkcolor(7);                     /* white */
   _clearscreen(_GWINDOW);

   /* now draw inside the playing card */
   /* _settextposition is always relative to the current window */

   _settextcolor(0);                   /* black */
   _settextposition(1, 1);
   _outtext("4");

   _settextcolor(4);                   /* red */
   _outtext("\003");

   _settextposition(3, 4);
   _outtext("\003");
}

void
clear_message(void)
{
   _settextwindow(25, 1, 25, 80);

   _setbkcolor(3);                     /* cyan */
   _clearscreen(_GWINDOW);
}

void
print_message(char *message)
{
   clear_message();

   _settextcolor(15);                  /* bright white */
   _outtext(message);
}

void
print_messagebox(char *message)
{
   _settextwindow(7, 21, 11, 61);

   /* shadow */

   _setbkcolor(0);                     /* black */
   _clearscreen(_GWINDOW);

   _settextwindow(6, 20, 10, 60);

   /* message box */

   _setbkcolor(4);                     /* red */
   _clearscreen(_GWINDOW);

   /* message in the message box */

   _settextcolor(14);                  /* yellow */
   _settextposition(2, 2);
   _outtext(message);

}

int
main()
{
   /* set new video mode: color, 80 cols */

   _setvideomode(_TEXTC80);
   _displaycursor(_GCURSOROFF);

   clear_tableau();
   clear_message();

   draw_card(1, 1, 4);

   /* quit */

   print_message("press any key to show a message");
   getch();
   clear_message();

   print_messagebox("press any key to exit");
   getch();

   _displaycursor(_GCURSORON);
   _setvideomode(_DEFAULTMODE);

   return 0;
}

By defining all the hard work of creating the Text User Interface elements in functions, the main function is relatively short. That makes for a main function that is much easier to read, and easier to debug later.

screenshot of Card program screenshot of Card program

REVIEW

Since conio is a nonstandard library, its implementation may differ depending on your compiler. Check your compiler's documentation or C library reference guide for what functions and capabilities are provided in its conio library.

For the OpenWatcom C Library Reference, see the OpenWatcom Documentation page, and download clib.pdf.

For the DJGPP C Library conio Reference, see the DJGPP conio functions page.

Writing Linux programs in C? Linux doesn't have a conio library; Unix doesn't really have the same concept of "console" that DOS has. Instead, Unix terminals use cursor addressing, which is similar but different. The ncurses library is the most common method to do full-screen applications. For more information, check out my 5-part series about ncurses programming on Linux Journal:

  1. Getting started with ncurses
  2. Creating an adventure game in the terminal with ncurses
  3. Programming in color with ncurses
  4. About ncurses colors
  5. Programming text windows with ncurses

PRACTICE

Now that you've learned the essentials of conio programming, try writing these programs to practice. You should also refer to your compiler's C library reference guide for specific conio functions and how to use them. You should be able to complete these sample programs using OpenWatcom C:

Practice program 1.

Write a "visual" version of the FreeDOS PAUSE command. Set the background to blue, and draw a red pop-up message with black drop-shadow that says "Press any key to continue." Wait for the user to press a key on the keyboard, then clear the screen and exit.

Write a "visual" version of the FreeDOS ECHO command. Set the background to cyan, and draw a brown pop-up message with blue drop-shadow that prints whatever text was passed to ECHO on the command line. Wait for the user to press a key on the keyboard, then clear the screen and exit.

Practice program 3.

Write a program that draws a progress bar in ten percent increments using a for loop. Display a message at the bottom of the screen that indicates the user should press a key for the next iteration on the progress bar. Center the progress bar as 50 characters wide on the screen, and three rows tall. Use a blue background on the screen, and draw the progress bar using solid black for the "uncompleted" portion and solid green for the "completed" portion of the progress bar.

Practice program 4.

Write a test program that reads the keyboard using getch and displays the result in a text window. (In OpenWatcom's conio, getch will return 0 if the user pressed an extended function key, and the next call too getch returns a value for the extended key.) Exit if the user enters Q or q.

Practice program 5.

Write a sample program that displays text in every color text color and background color combination. Since there are eight background colors, draw the text in 10-character columns, with each column representing a different background color.

Need help? Check out the sample solutions.