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
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:
_GCLEARSCREEN | Clear the entire screen |
---|---|
_GVIEWPORT | Clear the graphics view port or clip region |
_GWINDOW | Clear 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:
_DEFAULTMODE | The default text mode, the original text mode before running your program. |
---|---|
_TEXTBW40 | Black 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. |
_TEXTBW80 | Black 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. |
_TEXTMONO | Black 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. |
_TEXTC40 | Color text on a 25 row × 40 column screen. Usually for CGA displays, but also available in later display systems. |
_TEXTC80 | Color 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:
numtextrows | Number of text rows (for example, 25) |
---|---|
numtextcols | Number of text columns (usually 80 or 40) |
numcolors | Number of actual colors |
mode | The current video mode |
adapter | The video adapter connected to the system |
monitor | The display or monitor connected to the system |
The adapter
field contains one of these values:
_MDPA | The original IBM monochrome display/printer adapter |
---|---|
_HERCULES | Hercules monochrome adapter |
_CGA | The IBM color/graphics adapter |
_MCGA | The multi-color graphics array |
_EGA | The enhanced graphics adapter |
_VGA | The standard video graphics array |
_SVGA | A compatible SuperVGA display adapter |
The monitor
field contains one of these values:
_MONO | A regular monochrome display |
---|---|
_ANALOGMONO | An analog monochrome display |
_COLOR | A regular color display |
_ANALOGCOLOR | An analog color display |
_ENHANCED | An 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;
}
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.
R | G | B | Color | |
---|---|---|---|---|
0 | 0 | 0 | Black | |
0 | 0 | 1 | Blue | |
0 | 1 | 0 | Green | |
0 | 1 | 1 | Cyan | |
1 | 0 | 0 | Red | |
1 | 0 | 1 | Magenta | |
1 | 1 | 0 | Yellow | |
1 | 1 | 1 | White |
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.
i | R | G | B | Color | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | Black | |
0 | 0 | 0 | 1 | Blue | |
0 | 0 | 1 | 0 | Green | |
0 | 0 | 1 | 1 | Cyan | |
0 | 1 | 0 | 0 | Red | |
0 | 1 | 0 | 1 | Magenta | |
0 | 1 | 1 | 0 | Yellow | |
0 | 1 | 1 | 1 | White | |
1 | 0 | 0 | 0 | Bright Black | |
1 | 0 | 0 | 1 | Bright Blue | |
1 | 0 | 1 | 0 | Bright Green | |
1 | 0 | 1 | 1 | Bright Cyan | |
1 | 1 | 0 | 0 | Bright Red | |
1 | 1 | 0 | 1 | Bright Magenta | |
1 | 1 | 1 | 0 | Bright Yellow | |
1 | 1 | 1 | 1 | Bright 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.
i | R | G | B | Color | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | Black | |
0 | 0 | 0 | 1 | Blue | |
0 | 0 | 1 | 0 | Green | |
0 | 0 | 1 | 1 | Cyan | |
0 | 1 | 0 | 0 | Red | |
0 | 1 | 0 | 1 | Magenta | |
0 | 1 | 1 | 0 | Brown | |
0 | 1 | 1 | 1 | White | |
1 | 0 | 0 | 0 | Bright Black | |
1 | 0 | 0 | 1 | Bright Blue | |
1 | 0 | 1 | 0 | Bright Green | |
1 | 0 | 1 | 1 | Bright Cyan | |
1 | 1 | 0 | 0 | Bright Red | |
1 | 1 | 0 | 1 | Bright Magenta | |
1 | 1 | 1 | 0 | Bright Yellow | |
1 | 1 | 1 | 1 | Bright 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 f
oreground color (iRGB=0000 Black to iRGB=1111 Bright White), three bits to code the b
ackground color (RGB=000 Black to RGB=111 White) and one bit for the B
link 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 | |||
---|---|---|---|---|---|---|
0 | Black | 8 | Bright Black | |||
1 | Blue | 9 | Bright Blue | |||
2 | Green | 10 | Bright Green | |||
3 | Cyan | 11 | Bright Cyan | |||
4 | Red | 12 | Bright Red | |||
5 | Magenta | 13 | Bright Magenta | |||
6 | Brown | 14 | Bright Yellow | |||
7 | White | 15 | Bright 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.
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:
- Getting started with ncurses
- Creating an adventure game in the terminal with ncurses
- Programming in color with ncurses
- About ncurses colors
- 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 usinggetch
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.