Troubleshooters.Com Presents

Linux Productivity Magazine

Volume 3 Issue 5, May 2004

Curses, Part 1

Copyright (C) 2004 by Steve Litt. All rights reserved. Materials from guest authors copyrighted by them and licensed for perpetual use to Linux Productivity Magazine. All rights reserved to the copyright holder, except for items specifically marked otherwise (certain free software source code, GNU/GPL, etc.). All material herein provided "As-Is". User assumes all risk and responsibility for any outcome.

Have Steve Litt write you a quick application!

See also Troubleshooting Techniques of the Successful Technologist
and Rapid Learning: Secret Weapon of the Successful Technologist
by Steve Litt

[ Troubleshooters.Com | Back Issues |Troubleshooting Professional Magazine ]


 
Debugging is twice as hard as writing the code in the first place.
Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.

-- Brian Kernighan

CONTENTS

Editor's Desk

By Steve Litt
Naustigically I watched the oilchange tech punch data into his computer. It was a pretty program, blue background with yellow fields and black type. No clutter. No tiny print. No mouse. No UAE's, GPFs, or Bluescreens. I used to write programs like that back in the days when functionality was a priority. It was a DOS program.

No GUI. Sometimes they're called Command Line Interface (CLI) apps. Sometimes they're called character based apps. But they're always lightweight, robust, and quick to write.

Maybe I just don't have the smarts to write a GUI program. Maybe I'm just one of those naustalgic guys waxing eloquent about the good old days. Maybe I don't like featureful programs. Whatever it is, I have a lot of company.

The number of businesses still using DOS programs is astounding. Windows 3.0 came out 14 years ago! Y2K was 4 years ago. Why do you see those old DOS programs at so many businesses? Quick lube places, video stores, dentists and doctors offices, service businesses; all eschewing modern technology in favor of a user interface popular when the Fine Young Cannibals ruled the airwaves and the Berlin Wall fell. Why?

Simplicity.

Command line interfaces hardly taxed the 640 MB 486's of their era, so they're quick and flawless on today's computers. The letters are big enough to read even for those of us without 20/20 vision. They're easily used with a keyboard, which we all know is the fastest way to punch data.

Simplicity is having the program designer map out the user's activities, instead of providing the user with a huge menu of activities he can perform, and probably botch.

Hey, I'm the last guy to say GUI apps are worthless. I'm composing this web page on Mozilla Composer, and I'm very glad I can see titles, bylines and graphics the way you'll see them. If you want to take away my Gimp you'll have to pry it out of my cold, dead hands.

But simple data input -- who's kidding whom? GUI just gets in the way.

The one problem with command line interface apps was they were single pathed -- if you drilled into some complex data, and then discovered you needed a new client record, unless the program was designed "just so" you'd need to back all the way out, insert the new client record, then drill back down.

With a modern character based (CLI) data acquisition program, that's one problem you won't have. With Linux you could run multiple instances of the program, so if you needed a client record, you'd just run another instance of the program, punch the client record, then go back to the original session. Unlike DOS, Linux is multitasking.

If you need to create a simple data app, wouldn't it be nice to quickly whip out a command line interface app? Less design, less debugging, less field troubleshooting, more readable. Works anywhere. But how would you do that in Linux?

I have one word for you: Curses.

At its simplest, Curses is a library that can write any character, of any color and background, to any character position on a character based terminal, as well as acquiring keystrokes one at a time. These capabilites are sufficient to create sophisticated user interface libraries. I know, because I've created such libraries in Turbo C.

But Curses does much of the work for you. Curses comes with facilities for character based windows so you can temporarily overwrite the screen, knowing that when you close that window the underlying information will be unchanged. Curses comes with a very nice facility for creating menus. As someone who has created four different menuing tools (UMENU was just the last one), I really appreciate that. Better still, the Curses menu library can easily be used to create picklists. I've written two picklist tools. Writing them nearly killed me, and they weren't half as easy to use as the menuing facility that comes with Curses.

Can Curses be used as the basis as a Rapid Application Development tool to bang out a 5 table app in a day? That remains to be seen. Would such applications be accepted 14 years after the advent of Windows 3.0? At first that sounds doubtful, but then you visit your local dentist, hotel, or auto shop, and often as not their monitor displays a character based app.

The most productive programming environment I ever saw was Clarion 2.1. Could Curses be used to create a tool as productive? Clarion 2.1 was a hard act to follow, but I'll tell you this -- the menus, picklists and forms that made Clarion apps so wonderful from a user perspective are fairly easy in Curses. To the extent that configuration of forms, menus and picklists can be placed in data files instead of code, creating a RAD interpreter would be doable. Likewise, creating a front end to produce and modify those data files would be doable.

Could it be as simple as a menu executable, a picklist executable, and a form executable, all linked together with scripts? Do you think such built and tested utilities could produce one day apps? I wouldn't bet against it.

And one day apps might be the only future we have left. With our programming jobs being emailed to India at a prodigious rate, perhaps our future is the creation and support of one day apps and sell them to small businesses. Perhaps we survivors will become hired gun admin/programmer-analysts. We'll have to wait and see.

This month's Linux Productivity Magazine explains the fundimentals of Curses -- cursor placement, colors, windows, and boxes. Finally, we use those fundimentals to create a practical menu. Next month we'll finish our Curses excursion by creating data input screens and picklists. Armed with menus, data input screens and picklists, we can create a fast and friendly user interface for any data processing application.

If you're a Linux or free software user, of if you just want to learn more about these technologies, this is your magazine. Enjoy!
Steve Litt is the author of Samba Unleashed.   Steve can be reached at his email address.

Help Publicize Linux Productivity Magazine

By Steve Litt
Loyal readers, I need your help.

For months I've publicized Linux Productivity Magazine, expanding it from a new magazine to a mainstay read by thousands. There's a limit to what I can do alone, but if you take one minute to help, the possibilities are boundless.

If you like this magazine, please report it to one of the Linux magazines. Tell them the URL, why you like it, and ask them to link to it.

I report it to them, but they don't take it very seriously when an author blows his own horn. When a hundred readers report the magazine, they'll sit up and take notice.

Reporting is simple enough. Just click on one of these links, and report the magazine. It will take less than 5 minutes.

News Mag
Submission URL or Email address
Comments
Slashdot
http://slashdot.org/submit.pl
Just fill in the short form.
LinuxToday
http://linuxtoday.com/contribute.php3
Just fill in the short form.
Linux Weekly News
lwn@lwn.net
Just tell them the URL, why you like it.
NewsForge
webmaster@linux.com
Just tell them the URL, why you like it.
VarLinux
http://www.varlinux.org/vlox/html/modules/news/submit.php
Just fill in the short form.
LinuxInsider.Com,
Newsfactor Network
http://www.newsfactor.com/perl/contact_form.pl?to=contact
Just tell them the URL, why you like it.
The Linux Knowledge Portal
webmaster@linux-knowledge-portal.org
Just tell them the URL, why you like it.
OS News
http://www.osnews.com/submit.php
Just tell them the URL, why you like it.
DesktopLinux
http://www.desktoplinux.com/cgi-bin/news_post.cgi
Only for LPM issues involving the Linux desktop, not for programming or server issues.

If you really like this magazine, please take 5 minutes to help bring it to a wider audience. Submit it to one of the preceding sites.
Steve Litt is the founder and acting president of Greater Orlando Linux User Group (GoLUG).   Steve can be reached at his email address.

GNU/Linux, open source and free software

By Steve Litt
Linux is a kernel. The operating system often described as "Linux" is that kernel combined with software from many different sources. One of the most prominent, and oldest of those sources, is the GNU project.

"GNU/Linux" is probably the most accurate moniker one can give to this operating system. Please be aware that in all of Troubleshooters.Com, when I say "Linux" I really mean "GNU/Linux". I completely believe that without the GNU project, without the GNU Manifesto and the GNU/GPL license it spawned, the operating system the press calls "Linux" never would have happened.

I'm part of the press and there are times when it's easier to say "Linux" than explain to certain audiences that "GNU/Linux" is the same as what the press calls "Linux". So I abbreviate. Additionally, I abbreviate in the same way one might abbreviate the name of a multi-partner law firm. But make no mistake about it. In any article in Troubleshooting Professional Magazine, in the whole of Troubleshooters.Com, and even in the technical books I write, when I say "Linux", I mean "GNU/Linux".

There are those who think FSF is making too big a deal of this. Nothing could be farther from the truth. The GNU General Public License, combined with Richard Stallman's GNU Manifesto and the resulting GNU-GPL License, are the only reason we can enjoy this wonderful alternative to proprietary operating systems, and the only reason proprietary operating systems aren't even more flaky than they are now. 

For practical purposes, the license requirements of "free software" and "open source" are almost identical. Generally speaking, a license that complies with one complies with the other. The difference between these two is a difference in philosophy. The "free software" crowd believes the most important aspect is freedom. The "open source" crowd believes the most important aspect is the practical marketplace advantage that freedom produces.

I think they're both right. I wouldn't use the software without the freedom guaranteeing me the right to improve the software, and the guarantee that my improvements will not later be withheld from me. Freedom is essential. And so are the practical benefits. Because tens of thousands of programmers feel the way I do, huge amounts of free software/open source is available, and its quality exceeds that of most proprietary software.

In summary, I use the terms "Linux" and "GNU/Linux" interchangably, with the former being an abbreviation for the latter. I usually use the terms "free software" and "open source" interchangably, as from a licensing perspective they're very similar. Occasionally I'll prefer one or the other depending if I'm writing about freedom, or business advantage.

Steve Litt is the author of Troubleshooting Techniques of the Successful Technologist.   Steve can be reached at his email address.

Curses Hello World

By Steve Litt
Create the following test.c:

#include <ncurses.h>
main(int argc, char *argv)
{
char c;
initscr();
mvaddstr(20, 50, "HELLO WORLD");
mvaddstr(14, 10, "Press a letter==>");
c = getch();
endwin();
printf("\n\nYou pressed >%c<\n", c);
}

Notice several things about the preceding code:
Now compile and run it like this:

gcc -lncurses test.c
./a.out

The result looks like this:

Output of the Hello World program

Note that indeed, the second mvaddstr() prints above the first, and the cursor is moved to one past the end of the last string printed.

This article showed one of the simplest possible full screen apps. This 11 line program gets you in the door. Now let's make it a little nicer...
Steve Litt is the author of the Universal Troubleshooting Process Courseware.   Steve can be reached at his email address.

A Color App

By Steve Litt
Monochrome apps are wonderful for monochrome monitors, and for very simple applications. But for apps with several pieces of data to input, color is a wonderful way of calling your attention to various parts of the screen.

Let's take the prior app and turn it into a colored app:
#include <ncurses.h>
#define YELLOWONBLUE 1
#define BLACKONWHITE 2

void clrscr(void)
{
int y, x, maxy, maxx;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

main(int argc, char *argv)
{
char c;
initscr();

start_color();
init_pair(YELLOWONBLUE, COLOR_YELLOW, COLOR_BLUE);
init_pair(BLACKONWHITE, COLOR_BLACK, COLOR_WHITE);

attrset(COLOR_PAIR(YELLOWONBLUE));
clrscr();

attrset(COLOR_PAIR(BLACKONWHITE));
mvaddstr(20, 50, "HELLO WORLD");

attrset(COLOR_PAIR(YELLOWONBLUE));
mvaddstr(14, 10, "Press a letter==>");

c = getch();
endwin();
printf("\n\nYou pressed >%c<\n", c);
}


Let's start in the main routine. We perform the start_color() call right after initscr(). Now we have the facility for color. Next, we assign color combinations to integer constants YELLOWONBLUE and BLACKONWHITE. We could have simply used integers, but the constants provide both documentation and programmer error prevention.

The init_pair() calls assign their respective color combinations to lookup tables keyed by the integer first argument. That color combination can be retrieved using the COLOR_PAIR() function, which can then be used by attrset() to load that color combination. Any mvaddstr() call will write in whatever color combination has been activated at the time.

The clrscr() routine provides examples of curses facilities we haven't covered yet. The getmaxyx() macro has a WINDOW pointer and two integers as its arguments, setting the integers to the character dimensions of the window. Remember that getmaxyx() is a macro, so it can change its arguments without passing addresses. The stdscr first argument is a Curses global referring to the window comprising the entire screen.

Obtaining the number of lines and columns of the screen makes your code portable. The mvaddch() call writes a single character instead of a whole string. The clrscr() function paints the screen with the current background color, blue in this example.

Here's the result:

Color hello world

Attributes

This is the list of available colors, straight from ncurses.h:
There are 8 colors, not the 16 you remember from your DOS programming days. All these colors are fairly dark. The yellow is really a dingy brown. The white is more of a light gray. How do you get real yellow, and white white?

If you remember your character mode color programming for DOS, you remember  that colors went from 0 to 15, with the top 8 colors being the bright versions. Curses programming is different. To get bright colors you need to use attributes. Here are some attributes of interest:
The actual values of these constants needn't concern us. WA_NORMAL means no change to the color pair being shown, and is the default. The rest of these attributes mean different things on different terminals.

The WA_BOLD attribute does pretty much what you'd expect on most GUI terminal emulators. It makes dark foregrounds darker and light foregrounds lighter, so that with any reasonable color scheme, the WA_BOLD attributes increases contrast and therefore makes the writing seem "bolder".

However, on a genuine character terminal (the kind you get when you don't run X or when you press Ctrl+Alt+F2), WA_BOLD always makes the foreground color brighter. Therefore, if you have dark printing on a light background, WA_BOLD makes the printing visually less bold.

For high contrast screens on any terminal, try to use a light foreground on a dark background, and use WA_BOLD. Good foreground colors include white, yellow and cyan. Good background colors include black, blue, red and magenta. Such combinations will be uniformly readable almost anywhere.

If you must use dark print on a light background, unbolded black on white works pretty well everywhere. Cyan can also be effective as a background color. When using black or other dark colors in the foreground, you could query such environment variables as $TERM or $DISPLAY, and depending on their value use WA_BOLD or not. This would give you increased portablity within the UNIX world, but would of cours be totally ineffective in Windows or on the Mac.

My research indicates that the WA_BOLD attribute is the only universally safe attribute you can use. The others can cause underlining or other bizarre effects on some GUI terminal emulators.

If you remember your DOS programming from the 1980's, you remember that a genuine IBM-PC compatible VGA character terminal cannot produce the lighter backgrounds, such as:
Those colors can be backgrounds on GUI terminals, but not on genuine character terminals. Even on GUI terminals, there's no universal, foolproof way to paint them on the background. The least quirky way to do this on GUI terminals is to use WA_BLINK, which on GUI terminals makes the background bright. It also makes the app completely unportable because on a genuine character terminal all the text actually blinks.

The preceding is by no means portable. Try running the following program on several different GUI terminals, and note the inconsistencies:
#include <ncurses.h>
#define BLACKONWHITE 2

void clrscr(void)
{
int y, x, maxy, maxx;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

int main(int argc, char *argv[])
{
char c;
initscr();

start_color();
init_pair(BLACKONWHITE, COLOR_BLACK, COLOR_WHITE);

attrset(COLOR_PAIR(BLACKONWHITE) | WA_BOLD | WA_BLINK);
clrscr();

mvaddstr(10, 10, "BOLD BLINK");
mvaddstr(11, 10, "BOLD BLINK");

attrset(COLOR_PAIR(BLACKONWHITE) | WA_BOLD );
mvaddstr(14, 10, "BOLD ONLY");
mvaddstr(15, 10, "BOLD ONLY");

attrset(COLOR_PAIR(BLACKONWHITE) | WA_BLINK);
mvaddstr(18, 10, "BLINK ONLY");
mvaddstr(19, 10, "BLINK ONLY");

attrset(COLOR_PAIR(BLACKONWHITE));
mvaddstr(22, 10, "NEITHER: Press a letter==>");

c = getch();
endwin();
return(0);
}


Try the preceding code on several GUI terminals:
Unless you have complete uniformity of the terminals and terminal emulators on which your software will be used, the best bet is to use a dark background, and a light foreground, and WA_BOLD, or maybe black on a white (lightgray actually) background.

One question remains: What if you need to write a color app that will run in monochrome mode on monochrome monitors. Read on...
Steve Litt is the author of Rapid Learning: Secret Weapon of the Successful Technologist.   Steve can be reached at his email address.

Color on Demand

By Steve Litt
A good color app will run on a monochrome monitor. Who has monochrome monitors? Businesses whose data entry needs are simple and very specific. I just went into a business the other day and noticed everyone punching data on circa 1984 amber monitors.

You might want to create your app to work with both color and monochrome monitors. To do so, simply place the color extensions inside of subroutines that test for color ability, as follows:

#include <ncurses.h>
#define YELLOWONBLUE 1
#define BLACKONWHITE 2

bool initColors()
{
if(has_colors())
{
start_color();
init_pair(YELLOWONBLUE ,COLOR_YELLOW ,COLOR_BLUE );
init_pair(BLACKONWHITE ,COLOR_BLACK ,COLOR_WHITE );
return(true);
}
else
return(false);
}

bool setColors(int colorscheme)
{
if(has_colors())
{
attrset(colorscheme);
return(true);
}
else
return(false);
}

void clrscr(void)
{
int y, x, maxy, maxx;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

main(int argc, char *argv)
{
char c;
initscr();

initColors();
setColors(COLOR_PAIR(YELLOWONBLUE));
clrscr();

setColors(COLOR_PAIR(BLACKONWHITE));
mvaddstr(20, 50, "HELLO WORLD");
setColors(COLOR_PAIR(YELLOWONBLUE));
mvaddstr(14, 10, "Press a letter==>");
c = getch();
endwin();
printf("\n\nYou pressed >%c<\n", c);
}

The preceding encapsulates the initialization of colors in initColors(), and the setting of colors in setColors(). This allow full color use with color monitors and monochrome use with monochrome monitors.

This program outputs the same as the one in the preceding article:

Color hello world with facilities for monochrome
Steve Litt is the author of Samba Unleashed.   Steve can be reached at his email address.

Other Simple Curses Calls

By Steve Litt
Earlier articles in this issue discussed a bare subset of Curses functions:
This article discusses a few more basic Curses calls.

Movement and Output

Notice the mvaddch(int y, int x, char c) function, which writes character c at coordinates (y, x). This function really performs two distinct operations:
  1. Move the cursor to (y,x)
  2. Print c
If desired, you can separate these two functions as follows:

move(y, x);
addch(c);

You might want to move without immediately printing if you want the cursor for your next getch() to appear somewhere besides the end of the last write.

You might want to write a character without previously moving if you want the character appended to the previous. For instance, the following is a (suboptimal) way to write the word "Linux" at (12, 10):

mvaddch(12, 10, 'L');
addch('i');
addch('n');
addch('u');
addch('x');

Likewise, mvaddstr() can be split into move() and addstr().

Other Input and Output Functions

printw() has the same functionality as the standard IO packages printf(), except that it includes screen coordinates. According to the documentation I've read, in order for the screen to reflect the changes you've made with output functions, you must call the refresh() function. That has not been my experience, but if you see a window not reflecting what's been written to it, try the refresh() function.

We already discussed the getch() function. There's also a getstr() function that gets a string, but it's subject to buffer overflow problems. The getnstr() includes an integer maximum on bytes to be acquired, thereby eliminating the problem of buffer overflow. scanw("%d", &n_discs); is your facility for acquiring formatted input such as numbers. It has all the advantages and problems of the standard I/O library's scanf() function.

Other Color Functions

We've discussed attrset(), which sets all video attributes. There's also attron(), which sets only the attributes in the argument, and attroff(), which unsets only the attributes in the argument. If you have a case where you don't want to take absolute control of coloration, but instead just want to "tweak" the existing color a little bit, these functions do that. One obvious use is reverse video.
attron(WA_REVERSE);   /* temporarily make reverse video */
/* print reverse video stuff here */
attroff(WA_REVERSE); /* restore regular video */
What if you want to change the color and attributes of a portion of the screen, without writing anything? Meet the chgat() function:

#include <ncurses.h>
#include <string.h> /* accommodate strlen() */

#define YELLOWONBLUE 1

void clrscr(void)
{
int x, y, maxx, maxy;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

int main(int argc, char *argv[])
{
int y_start, x_start, length, attrib, colorindex;
char lastname[]="Anderson";

initscr();
start_color();
init_pair(YELLOWONBLUE, COLOR_YELLOW, COLOR_BLUE);
attrset(COLOR_PAIR(YELLOWONBLUE) | WA_BOLD);
clrscr();

mvprintw(2,0,"Last name: %s", lastname);
mvprintw(4,0,"Use -1 length to reverse everything from column 11 to end of line.", lastname);

/***************************************************************/
/* REVERSE VIDEO A LAST NAME */
/***************************************************************/
y_start = 2;
x_start = 11;
length = strlen(lastname);
attrib = A_REVERSE;
colorindex = YELLOWONBLUE;
move(y_start, x_start);
chgat(length, attrib, colorindex, NULL);
move(0, 0);

/***************************************************************/
/* USE -1 LENGTH TO REVERSE VIDEO TO THE END OF THE LINE */
/***************************************************************/
y_start = 4;
x_start = 11;
length = -1;
attrib = A_REVERSE;
colorindex = YELLOWONBLUE;
move(y_start, x_start);
chgat(length, attrib, colorindex, NULL);
move(0, 0); /* move cursor out of the area of interest */

/***************************************************************/
/* FINISH UP */
/***************************************************************/
refresh();
getch();
endwin();
return 0;
}


Starting at the current cursor position, chgat() changes the video attributes to its second argument, with the color pair index specified in the third argument, for the length specified in the first argument. If the first argument is -1 then the attribute change goes to the end of the line. There is also a mvchgat() function that performs the move and the attribute change in one call.

Window Functions

This has nothing to do with the Microsoft operating system called "Windows", which somehow acquired a trademark on that completely descriptive name. With respect to Curses, a window is simply a part of the available screen which experiences input/output as a unit, very much like a window in the Windows operating system.

Actually, ALL Curses functions are related to a window. The initscr() function instantiates one window, called stdscr. All the function calls reviewed so far in this document operate on that one default window. All these function calls have equivalents where you can name the window on which you're operating:

stdscr function
General window function
Purpose
addch(c) waddch(windowPointer, c) Place character at current cursor
addstr(string) waddstr(windowPointer, string) Place string at current cursor
printw(format, ...) wprintw(windowPointer, format, ...) Place formatted string at current cursor



move(y, x) wmove(windowPointer, y, x) Move cursor to y, x
mvaddch(y, x, c) mvwaddch(windowPointer, y, x, c) Place character at y, x
mvaddstr(y, x, string) mvwaddstr(windowPointer, y, x, string) Place string at y, x
mvprintw(y, x, format, ...) mvwprintw(windowPointer, y, x, format, ...) Place formatted string at y, x



attrset(colorpair)
wattrset(windowPointer, colorpair)
Replaces all current attributes with colorpair
attron(colorpair) wattron(windowPointer, colorpair) Or's colorpair with current attributes
attroff(colorpair) wattroff(windowPointer, colorpair) Turn off colorpair attribute, leave others alone
chgat(length, attr, colorindex)
mvchgat(y, x, length, attr, colorindex)
chwgat(window, length, attr, colorindex)

mvwchgat(window, y, x, length, attr, colorindex)
Change attribute and color of part of a line without overwriting the existing text.



getch() wgetch(windowPointer) Acquire keystroke
getstr(buffer) wgetstr(windowPointer, buffer) Acquire line from user
getnstr(buffer) wgetnstr(windowPointer, buffer) Acquire line, limited to n chars
scanw(format, ...)
wscanw(format, ...)
Acquire formatted input



mvgetch(y, x) mvwgetch(windowPointer, y, x) Move to y, x, then acquire char
mvgetstr(y, x, buffer) mvwgetstr(windowPointer, y, x, buffer) Move to y, n, then acquire string
mvgetnstr(y, x, buffer) mvwgetnstr(windowPointer, y, x, buffer) Move to y, n, then acquire limited string
mvwscanw(y, x, format, ...)
mvwscanw(windowPointer, y, x, format, ...)
Move to y, n, then acquire formatted input

Simple programs require only the standard screen window. However, if you need several input "screens" in a single program, or to make box drawing easier, consider using the general window pointer versions.
Steve Litt is the author of the Universal Troubleshooting Process Courseware.   Steve can be reached at his email address.

Special characters

By Steve Litt
Who wants CLI forms without all the nice little borders you can draw? It was easy in the days of DOS and ANSI.SYS. It still is. Try this:

#include <ncurses.h>

#define YELLOWONBLUE 1
#define BLACKONWHITE 2
#define WHITEONBLACK 3

/* DEFINE APP WIDE COLORS/ATTRIBS */
#define ATTRIBS WA_BOLD
#define COLORS YELLOWONBLUE

bool initColors()
{
if(has_colors())
{
start_color();
init_pair(YELLOWONBLUE ,COLOR_YELLOW, COLOR_BLUE );
init_pair(BLACKONWHITE ,COLOR_BLACK ,COLOR_WHITE );
init_pair(WHITEONBLACK ,COLOR_WHITE ,COLOR_BLACK );
return(true);
}
else
return(false);
}

bool setColors(int colorscheme)
{
if(has_colors())
{
attrset(colorscheme);
return(true);
}
else
return(false);
}

void clrscr(void)
{
int y, x, maxy, maxx;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

int main(int argc, char *argv[])
{
char c;

initscr();
initColors();
attrset(COLOR_PAIR(COLORS) | ATTRIBS);
clrscr();

box(stdscr, ACS_VLINE, ACS_HLINE);
mvaddstr(21, 10, "Press key to end==>");
c = getch();
endwin();
printf("\n\nYou pressed >%c<\n", c);
return(0);
}


The resulting screen looks like this:
Screen with single line box surrounding it

Now let's write a little app to show all available linedraw characters. This app also prints the title where you'd expect it on a CLI app:

#include <ncurses.h>
#include <string.h> /* facilitate strlen() */

#define YELLOWONBLUE 1
#define BLACKONWHITE 2
#define WHITEONBLACK 3

/* DEFINE APP WIDE COLORS/ATTRIBS */
#define ATTRIBS WA_BOLD
#define COLORS YELLOWONBLUE

bool initColors()
{
if(has_colors())
{
start_color();
init_pair(YELLOWONBLUE ,COLOR_YELLOW, COLOR_BLUE );
init_pair(BLACKONWHITE ,COLOR_BLACK ,COLOR_WHITE );
init_pair(WHITEONBLACK ,COLOR_WHITE ,COLOR_BLACK );
return(true);
}
else
return(false);
}

bool setColors(int colorscheme)
{
if(has_colors())
{
attrset(colorscheme);
return(true);
}
else
return(false);
}

void clrscr(void)
{
int y, x, maxy, maxx;
getmaxyx(stdscr, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvaddch(y, x, ' ');
}

void investigate_box_chars(void)
{
move(3, 10); addch(' ');
addch(ACS_ULCORNER); addch(' ');
addch(ACS_LLCORNER); addch(' ');
addch(ACS_URCORNER); addch(' ');
addch(ACS_LRCORNER); addch(' ');
addch(ACS_LTEE); addch(' ');
addch(' ');

move(5, 10); addch(' ');
addch(ACS_RTEE); addch(' ');
addch(ACS_BTEE); addch(' ');
addch(ACS_TTEE); addch(' ');
addch(ACS_HLINE); addch(' ');
addch(ACS_VLINE); addch(' ');
addch(' ');

move(7, 10); addch(' ');
addch(ACS_PLUS); addch(' ');
addch(ACS_S1); addch(' ');
addch(ACS_S9); addch(' ');
addch(ACS_DIAMOND); addch(' ');
addch(ACS_CKBOARD); addch(' ');
addch(' ');

move(9, 10); addch(' ');
addch(ACS_DEGREE); addch(' ');
addch(ACS_PLMINUS); addch(' ');
addch(ACS_BULLET); addch(' ');
addch(ACS_LARROW); addch(' ');
addch(ACS_RARROW); addch(' ');
addch(' ');

move(11, 10); addch(' ');
addch(ACS_DARROW); addch(' ');
addch(ACS_UARROW); addch(' ');
addch(ACS_BOARD); addch(' ');
addch(ACS_LANTERN); addch(' ');
addch(ACS_BLOCK); addch(' ');
addch(' ');

move(13, 10); addch(' ');
addch(ACS_S3); addch(' ');
addch(ACS_S7); addch(' ');
addch(ACS_LEQUAL); addch(' ');
addch(ACS_GEQUAL); addch(' ');
addch(ACS_PI); addch(' ');
addch(' ');

move(15, 10); addch(' ');
addch(ACS_NEQUAL); addch(' ');
addch(ACS_STERLING); addch(' ');
addch(ACS_BSSB); addch(' ');
addch(ACS_SSBB); addch(' ');
addch(ACS_BBSS); addch(' ');
addch(' ');

move(17, 10); addch(' ');
addch(ACS_SBBS); addch(' ');
addch(ACS_SBSS); addch(' ');
addch(ACS_SSSB); addch(' ');
addch(ACS_SSBS); addch(' ');
addch(ACS_BSSS); addch(' ');
addch(' ');

move(19, 10); addch(' ');
addch(ACS_BSBS); addch(' ');
addch(ACS_SBSB); addch(' ');
addch(ACS_SSSS); addch(' ');
}

void wprintTitleCentered(WINDOW *win, const char *titleText)
{
int x, maxy, maxx;

getmaxyx(win,maxy,maxx);
x = (maxx - 4 - strlen(titleText))/2;
mvwaddch(win, 0, x, ACS_RTEE);
waddch(win, ' ');
waddstr(win, titleText);
waddch(win, ' ');
waddch(win, ACS_LTEE);
}

int main(int argc, char *argv[])
{
char c;
int maxy, maxx;

/* INITIALIZE */
initscr();
initColors();
setColors(COLOR_PAIR(COLORS) | ATTRIBS);
clrscr();

/* DRAW THE BOX AND SPLIT IT AT COLUMN 64 */
box(stdscr, ACS_VLINE, ACS_HLINE);
wprintTitleCentered(stdscr, "Listing of ACS Characters");

/* SPLIT IT AT COLUMN 64 */
getmaxyx(stdscr,maxy,maxx);
mvvline(1, 64, ACS_VLINE, maxy-2);
mvaddch(0, 64, ACS_TTEE);
mvaddch(maxy - 1, 64, ACS_BTEE);

/* SPLIT RIGHT SECTION AT LINE 10 */
mvhline(10, 65, ACS_HLINE, maxx-2);
mvaddch(10, 64, ACS_LTEE);
mvaddch(10, maxx - 1, ACS_RTEE);

/* WRITE LIST OF THE LINEDRAW CHARS */
investigate_box_chars();

/* PROMPT USER TO END APP */
mvaddstr(21, 10, "Press key to end==>");
c = getch();

/* END CURSES ENVIRONMENT, AND REPORT ON KEYSTROKE */
endwin();
printf("\n\nYou pressed >%c<\n", c);
return(0);
}

In the preceding, note:
Here's the resulting screen:
Screen showing list of linedraw characters

Linedraw characters are great, but their primary purpose is to delineate windows. Read on...
Steve Litt is the author of the Universal Troubleshooting Process Courseware.   Steve can be reached at his email address.

Windows

By Steve Litt
Bill Gates, eat your heart out, Curses does windows. Applications display menus calling picklists calling forms with fields calling picklists on and on ad infinitum. When a picklist temporarily covers part of a form, the form must be restored once it's gone. That's where windows come in.

Windows in Curses are a challenge. There are many versions of Curses, each handling windows a little differently. Different operating systems display a little differently. Even graphical terminals such as aterm, xterm, konsole, rxvt and gnome_terminal display a little differently. Especially important is the font. If a GUI terminal program uses a proportional font, Curses programs will look wrong, especially windowing.

This article represents my best effort to create a good windowing program. Please understand that I have only Linux, and cannot possibly test on many environments. If you find improvements, please email me.

Before reviewing the following simple demonstration of overlapping windows, please keep the following in mind:
To demonstrate the principles of overlapping screens, we create a tiny program that does the following:
Here's the code:

#include <ncurses.h>

int main(int c, char *argv[])
{
WINDOW *base_win, *small_win;
int maxy, maxx;

/* INITIALIZE CURSES AND REFRESH THE STANDARD SCREEN */
initscr();
getmaxyx(stdscr, maxy, maxx);
touchwin(stdscr);
wrefresh(stdscr); /* I don't know why this is necessary, but it is! */

/* CREATE AND DISPLAY THE BASE WINDOW */
base_win = newwin(maxy, maxx, 0,0);
box(base_win, 0, 0);
mvwaddstr(base_win, 7,6,"Press enter to continue, if you please==>");
touchwin(base_win);
wrefresh(base_win);
getch();

/* CREATE AND DISPLAY THE SMALL WINDOW */
small_win = newwin(10, 30, 3,3);
box(small_win, 0, 0);
mvwaddstr(small_win, 1,1,"small win");
mvwaddstr(small_win, 2,2,"small win");
mvwaddstr(small_win, 3,3,"Enter=continue=>");
touchwin(base_win);
wrefresh(small_win);
getch();

/* BURY SMALL WINDOW */
touchwin(base_win);
wrefresh(base_win);
getch();

/* UNBURY SMALL WINDOW */
touchwin(small_win);
wrefresh(small_win);
getch();

/* DELETE SMALL WINDOW */
delwin(small_win);
touchwin(base_win);
wrefresh(base_win);
getch();

/* END CURSES */
endwin();
printf("\n\nwin.c\n");
return(0);
}

And here is the sequence of screens you see at each getch():
The base window
The base window
The small window appears
The small window appears
The base window is moved to the front
The base window is moved to the front, so the small window is buried.
The small window is moved back to the front
The small window is moved back to the front.
The small window has been destroyed
The small window has been destroyed.

Colors, Titles, Derived Windows, Text and Function Keys

The preceding overlapping window example was great for proving the concept. In real life, we want overlapping windows to be different colors so the user has immediate and obvious context information concerning where he or she is. We also want titles on the windows, and we want text instructions -- perhaps with a few extra lines denoting them. All of this is possible. Additionally, we want function keys to be recognized by getch().

Color Gotchas

Colors, especially background colors, are much more difficult on Linux/Curses than they were in DOS/Windows/TurboPascal. In the DOS world, colors were numbered 0 to 15:

{ Foreground and background color constants }
Black = 0;
Blue = 1;
Green = 2;
Cyan = 3;
Red = 4;
Magenta = 5;
Brown = 6;
LightGray = 7;

{ Foreground color constants }
DarkGray = 8;
LightBlue = 9;
LightGreen = 10;
LightCyan = 11;
LightRed = 12;
LightMagenta = 13;
Yellow = 14;
White = 15;


Colors 8 through 15 are neither defined nor available in Curses. The only way to produce them is to combine your color pair with an attribute such as WA_BOLD, WA_BLINK, WA_REVERSE or WA_DIM. Using such combinations, you can get all 15 colors, either foreground or background, on a GUI terminal such as xterm, aterm or rxvt. Trouble is, especially with the bright background colors, what works on one terminal emulator might not work on another, and will almost certainly cause troubles on a CLI terminal.

True CLI terminals cannot use colors 8 through 15 for the background. My experiments so far have shown that WA_BOLD creates a lighter foreground on a true CLI terminal, but it creates what you would expect (darker foreground with light backgrounds, or ligher foreground with dark backgrounds) on a GUI terminal.

The bottom line is that if you need to program for a variety of terminals, or unknown terminals, you'll have some portability problems.

Because of the fact that WA_BOLD does what you want on GUI terminals but uniformly lightens on true CLI, your best bet is to use a light foreground color so that WA_BOLD increases contrast in either case. White and yellow are excellent foreground choices, with cyan being reasonable if you're using a black background.

If you're using a light foreground, naturally you'll want a dark background. That works out wonderfully, because both CLI and GUI terminals faithfully reproduce the darker (0-7) background colors. GUI terminals can go light with their backgrounds, but doing so messes up portability, even between different GUI terminals. Black, blue, red and magenta make good backgrounds for light foregrounds. Green is somewhat readable with light foregrounds. Avoid white, yellow and cyan backgrounds with light foregrounds.

If you absolutely have to use a dark foreground, black on white and black on cyan are the best. Note, however, that whatever your choice for WA_BOLD, it will look good on one but not the other. If you want black on white looking good everywhere, you'll need a way of deducing what kind of terminal you're using, and adjust WA_BOLD accordingly. Otherwise, white on black looks reasonable everywhere.

If you choose to step beyond the suggestions in the previous few paragraphs, you're inviting all softs of varying behavior depending on terminal and environment, as well as using very obscure attributes that can do strange things on various GUI terminals (like causing underlining).

Now that we've discussed color gotchas, let's colorize the window demonstration detailed earlier in this article. We'll also add centered titles on each window's top line, using the wCenterTitle() function:

#include <ncurses.h>
#include <string.h> /* facilitate strlen() */

#define WHITEONRED 1
#define WHITEONBLUE 2
#define WHITEONBLACK 3
#define BLACKONWHITE 4


void wCenterTitle(WINDOW *pwin, const char * title)
{
int x, maxy, maxx, stringsize;
getmaxyx(pwin, maxy, maxx);
stringsize = 4 + strlen(title);
x = (maxx - stringsize)/2;
mvwaddch(pwin, 0, x, ACS_RTEE);
waddch(pwin, ' ');
waddstr(pwin, title);
waddch(pwin, ' ');
waddch(pwin, ACS_LTEE);
}

void wclrscr(WINDOW * pwin)
{
int y, x, maxy, maxx;
getmaxyx(pwin, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvwaddch(pwin, y, x, ' ');
}

int main(int c, char *argv[])
{
WINDOW *base_win, *small_win;
int maxy, maxx;

/* INITIALIZE CURSES AND COLORS AND REFRESH THE STANDARD SCREEN */
initscr();
getmaxyx(stdscr, maxy, maxx);
start_color();
init_pair(WHITEONRED, COLOR_WHITE, COLOR_RED);
init_pair(WHITEONBLUE, COLOR_WHITE, COLOR_BLUE);
init_pair(WHITEONBLACK, COLOR_WHITE, COLOR_BLACK);
init_pair(BLACKONWHITE, COLOR_BLACK, COLOR_WHITE);
wrefresh(stdscr); /* I don't know why this is necessary, but it is! */

/* CREATE AND DISPLAY THE BASE WINDOW */
base_win = newwin(maxy, maxx, 0,0);
wattrset(base_win, COLOR_PAIR(WHITEONBLUE) | WA_BOLD);
wclrscr(base_win);
box(base_win, 0, 0);
wCenterTitle(base_win, "Large Window");
mvwaddstr(base_win, 7,6,"Press enter to continue, if you please==>");
touchwin(base_win);
wrefresh(base_win);
getch();

/* CREATE AND DISPLAY THE SMALL WINDOW */
small_win = newwin(10, 30, 3,3);
wattrset(small_win, COLOR_PAIR(WHITEONRED) | WA_BOLD);
wclrscr(small_win);
box(small_win, 0, 0);
wCenterTitle(small_win, "Small Window");
mvwaddstr(small_win, 1,1,"small win");
mvwaddstr(small_win, 2,2,"small win");
mvwaddstr(small_win, 3,3,"Enter=continue=>");
touchwin(small_win);
wrefresh(small_win);
getch();

/* BURY SMALL WINDOW */
touchwin(base_win);
wrefresh(base_win);
getch();

/* UNBURY SMALL WINDOW */
touchwin(small_win);
wrefresh(small_win);
getch();

/* DELETE SMALL WINDOW */
delwin(small_win);
touchwin(base_win);
wrefresh(base_win);
getch();

/* END CURSES */
endwin();
printf("\n\nwin.c\n");
return(0);
}

And here is the sequence of screens you see at each getch():

The base window
The base window
The small window appears
The small window appears
The base window is moved to the front
The base window is moved to the front, so the small window is buried.
Small window back in front
The small window is moved back to the front.
Small window has been destroyed
The small window has been destroyed.

You can see how the coloration makes it so much more evident what's on top, and which area the user is interacting with. The titles at the top of each window, surrounded by Tee's, make it even more evident. As mentioned earlier in this article, for portability we use light colors in the foreground (white), and dark colors in the background (blue and red).

Panels

The "best" way to create overlapping windows is with the Panel library. This library puts the windows in a stack. Panels aren't described in this month's Linux Productivity Magazine because the materials you learn in this issue will make you expert enough to figure out how to use panels. Suffice it to say you'll need to include panel.h, and  put  -lpanel.c in your compile command. Sample panel programs can be found with your example programs downloadable from links in the NCURSES Programming HOWTO by Pradeep Padala (URL in URL's section of this magazine).
Steve Litt is the author of the Universal Troubleshooting Process Courseware.   Steve can be reached at his email address.

What We've Accomplished

By Steve Litt
So far we've achieved cursor addressable character output with full foreground and background color, complete with box and line drawing. We've implemented an immediate keyboard read. We've also worked with overlapping windows leading the user's attention to specific parts of the screen.

Armed only with these things, we could build library routines to construct forms, input fields, menus and picklists. I've created such libraries many times in the DOS world. Fortunately, the Curses library has already implemented simple routines for forms, input fields, menus and picklists. Read on...
Steve Litt is the author of Rapid Learning: Secret Weapon of the Successful Technologist.   Steve can be reached at his email address.

A Simple Menu

By Steve Litt
Menus, forms and picklists are typically the three building blocks of a data input app. Menus are used to acquire the user's choice concerning what functions he or she wants to perform.

The Curses environment has built in menus. They're not trivial, but as the author of UMENU I can tell you that creating menus with Curses is a heck of a lot easier than writing them from scratch, especially if you want to enable the up and down arrows.

The following code implements a simple menu:


#include <curses.h>
#include <menu.h>
#include <stdlib.h> /* support calloc() */

int runMenu(char *choices[])
{
ITEM **my_items; /* list of items on this menu */
int c; /* key pressed */
MENU *my_menu; /* the menu structure */
int n_choices; /* number of items on menu */
int ssChoice; /* subscript to run around the choices array */
int my_choice = -1; /* the zero based numeric user choice */


/* CALCULATE NUMBER OF MENU CHOICES */
for(n_choices=0; choices[n_choices]; n_choices++);

/* ALLOCATE ITEM ARRAY AND INDIVIDUAL ITEMS */
my_items = (ITEM **)calloc(n_choices + 1, sizeof(ITEM *));
for(ssChoice = 0; ssChoice < n_choices; ++ssChoice)
my_items[ssChoice] = new_item(choices[ssChoice], NULL);
my_items[n_choices] = (ITEM *)NULL;

/* CREATE THE MENU STRUCTURE AND DISPLAY IT */
my_menu = new_menu((ITEM **)my_items);
post_menu(my_menu);
refresh();

/* HANDLE USER KEYSTROKES */
while(my_choice == -1)
{
c = getch();
switch(c)
{
case KEY_DOWN:
menu_driver(my_menu, REQ_DOWN_ITEM);
break;
case KEY_UP:
menu_driver(my_menu, REQ_UP_ITEM);
break;
case 10: /* Enter */
my_choice = item_index(current_item(my_menu));

/* RESET CURSOR IN CASE MORE SELECTION IS NECESSARY */
refresh();
pos_menu_cursor(my_menu);
break;
}
}

/* FREE ALL ALLOCATED RESOURCES */
free_item(my_items[0]);
free_item(my_items[1]);
free_menu(my_menu);

/* RETURN THE ZERO BASED NUMERIC USER CHOICE */
return(my_choice);
}

int main()
{
char *choices[] = /* The menu choices */
{
"One",
"Two",
"Three",
"Four",
"Cancel",
NULL
};
int choiceno;

initscr(); /* start ncurses */
cbreak(); /* immediately acquire each keystroke */
noecho(); /* do not echo user keystrokes */
keypad(stdscr, TRUE); /* enable detection of function keys */

choiceno = runMenu(choices); /* acquire the user's choice */

endwin(); /* end ncurses */

printf("\n\nYou chose item %d, %s\n", choiceno, choices[choiceno]);
return(0);
}

The preceding program's runMenu() function takes a NULL terminated array of strings as an argument, displays those strings as a menu, and returns the zero based subscript of the item chosen by the user. Here's the resulting screen display:
A simple menu


Note that there's no easy way to move the menu from its default top left position. In general, menus should be in their own windows.

Keystroke Responsive Menu

By Steve Litt
The best thing about UMENU is that it's fast. The user presses one key, and the choice is invoked. The menu described in the preceding section requires movement with an arrow key, and then pressing Enter. That's a good "hello world", but it's a slow user interface.

Menus can be tolerant or intolerant of multiple items with the same menu letter. UMENU is intolerant -- it's considered an error to have two items with the same letter. Many menus allow duplicate letters on a menu.

There are two ways of handling user keystrokes on multiple item tolerant menus:
  1. Building search patterns
  2. Repeated first character toggles

Building Search Patterns

One method of handling user alphanumeric keystrokes is to retain a buffer of every alphanumeric keystroke in such a way that the menu searches on keystrokes you pressed. For instance, in a menu of last names, if you press 'g', perhaps it takes you to Garner. If you then press 'o' perhaps it takes you to Gold. Pressing 'l' and 'd' leave you on "Gold", but if you press 'b' it brings you to "Goldberg", whereas if you had pressed 'c' or 'd' it might have taken you to "Goldenberg", and if you had pressed 'f', 'l' or 's' it might have taken you to "Goldstein".

This is the natural way of Curses menus as defined by menu.h. It is wonderful for picklists, or for menus with many, many items. Unfortunately it requires many keystrokes, it can be confusing on menus with small numbers of choices, and programming it to react immediately upon choosing a unique keystroke can be problematic. We'll revisit this option when we build a Curses picklist next month, but for now we'll put it aside in favor of the repeated first character toggles method.

Repeated First Character Toggles

This method typically is used for menus of few items. When the user presses a letter, if there is only one choice beginning with that letter, the menu terminates, returning the numeric value of that choice. That way, the user needn't press the Enter key. It's a lightning fast interface when the user presses a key corresponding to a unique choice.

But what if there are three items beginning with 't', and the user presses 't'. In that case, the highlight goes to the next item beginning with 't'. But what is meant by next item? Here's the logic. The user presses a key. Starting at the item after the current highlighted item, search for the next item beginning with 't', and highlight it. The reason we start after the current one is that if the current one started with 't', we'd stay there no matter how many times we pressed the 't' key.

But what if there are no items beginning with 't' following the current highlight? In that case, we start searching from item 0 and highlight the first item beginning with 't'. We know there will be one, because if there were only one, the menu would have already terminated, returning that unique choice.

If the user presses a key for which there is no corresponding item, nothing happens, or maybe you issue a beep.

This logic is implemented with the following data structure:

typedef struct
{
int matches; /* number of menu items matching keystroke */
int firstmatch; /* first choice to match it */
int followingmatch; /* first matching choice after current choice */
} MATCHSTRUCT;


The data structure is initialized to zeroes in all three fields. It is filled by a function that knows the list of choice names, the key character pressed, and the currently highlighted row. It iterates through every choice starting from the first choice, proceding until it sees a null choice.

For each choice, if the choice's first letter matches the user's keystroke, increment the matches field. On the first match, it also places the zero based number of the matching item in firstmatch. On the first match following the current highlighted choice, it places zero based number of that matching item in followingmatch.

So by the time all items have been iterated, if matches is zero, then the highlight should not be moved. If  matches is 1, then the item whose number is in firstmatch should be executed without further keystrokes. If matches is more than 1, then if followingmatch is nonzero then that should be the new highlighted item. Otherwise, firstmatch should be highlighted.

The preceding logic is simple enough, although its implementation can get a little hairy, especially because it involves the MENU and ITEM structures from menu.h. Here's the function that fills the MATCHSTRUCT structure. The function is called getMatch().

MATCHSTRUCT *getMatch(MENU *pmenu, char c)
{
/* DECLARE AND INITIALIZE VARIABLES */
const char * itemName;
int itemNo=0;
static MATCHSTRUCT matchstruct;
int currentChoiceNo = item_index(current_item(pmenu));

/* ZERO OUT TOTALS */
matchstruct.matches=0;
matchstruct.firstmatch=0;
matchstruct.followingmatch=0;

/* ITERATE THROUGH ALL ITEMS, STARTING WITH ITEM 0 */
/* THE FINAL ENTRY IN THE ITEM ARRAY IS ALWAYS A NULL POINTER */
while((itemName = item_name(menu_items(pmenu)[itemNo])) != NULL)
{
if(toupper(*itemName) == toupper(c)) /* match? */
{
if(matchstruct.matches == 0) /* first match gets recorded as such */
{
matchstruct.firstmatch = itemNo;
}
matchstruct.matches++; /* update the match total */
if((itemNo > currentChoiceNo)) /* first match after current highlight */
{
matchstruct.followingmatch = itemNo;
currentChoiceNo = 32000; /* prevent further follow matches */
}
}
itemNo++; /* next item */
}
return(&matchstruct);
}

The purpose of the preceding is to update three totals: matches, firstmatch and followingmatch. Each item name is retrieved from the item array of the menu using item_name(menu_items(pmenu)[itemNo]). pmenu is the *MENU menu pointer. menu_items() retrieves the array of item pointers from its menu pointer argument. [itemNo] indexes the array of item pointers, resolving to a single item pointer. item_name() retrieves the item name from that single item pointer. The name is compared to NULL, and as long as it's not NULL we check for matches. Please remember that in this application, we placed a NULL at the end of the array of menu names. Had we used an integer to define the end of the item pointer array, we would have needed to change this loop.

Once we have an item name corresponding to a zero based item number, the rest is easy. First we deduce whether it matches. If not, we do nothing. If so, we record the item number in firstmatch if there have been no matches. Then we increment matches. Finally, if this match has a larger item number than the current highlighted item, we assign this item number to followingmatch and then assign a huge (magic) number to currentChoiceNo to prevent later matches from updating followingmatch. Obviously, if we had more than 32000 items this algorithm would fail, but the whole idea of repeated first character toggles would be impractical with more than a few hundred items, so we're making a fair assumption.

You might wonder how we got the item number of the current highlighted item. The menu pointer contains that information if you dig deep enough. That digging occurred on this line:
int currentChoiceNo = item_index(current_item(pmenu));
Once the three totals are filled, the MATCHSTRUCT pointer is returned so that the calling function has access to the totals for use in deducing whether to directly execute the choice, or deducing where to move the highlight.

The Menu Function

As in the article on the simple menu, the runMenu() function runs the menu, and returns the user's choice as a number. As in the simple menu article, the runMenu() function moves up with the uparrow key, down with the downarrow key, and chooses with the Enter key (10). The main difference is that this program's menu function has a default in the switch statement, and that default calls getMatch() to get the totals with which to decide whether to do nothing, move the highlight, or return with the choice chosen.

int runMenu(char *choices[])
{
ITEM **my_items; /* list of items on this menu */
int c; /* key pressed */
MENU *my_menu; /* the menu structure */
int n_choices; /* number of items on menu */
int ssChoice; /* subscript to run around the choices array */
int my_choice = -1; /* the zero based numeric user choice */


/* CALCULATE NUMBER OF MENU CHOICES */
for(n_choices=0; choices[n_choices]; n_choices++);

/* ALLOCATE ITEM ARRAY AND INDIVIDUAL ITEMS */
my_items = (ITEM **)calloc(n_choices + 1, sizeof(ITEM *));
for(ssChoice = 0; ssChoice < n_choices; ++ssChoice)
my_items[ssChoice] = new_item(choices[ssChoice], NULL);
my_items[n_choices] = (ITEM *)NULL;

/* CREATE THE MENU STRUCTURE AND DISPLAY IT */
my_menu = new_menu((ITEM **)my_items);
menu_opts_off(my_menu, O_NONCYCLIC);
post_menu(my_menu);
refresh();

/* HANDLE USER KEYSTROKES */
while(my_choice == -1)
{
c = getch();
switch(c)
{
case KEY_DOWN:
menu_driver(my_menu, REQ_DOWN_ITEM);
break;
case KEY_UP:
menu_driver(my_menu, REQ_UP_ITEM);
break;
case 10: /* Enter */
my_choice = item_index(current_item(my_menu));

/* RESET CURSOR IN CASE MORE SELECTION IS NECESSARY */
refresh();
pos_menu_cursor(my_menu);
break;
default:
{
MATCHSTRUCT * ms; /* matches, firstmatch, followingmatch */

/* MATCH ONLY PRINTABLE KEYSTROKES */
if((c <= ' ') || (c > '~')) break;

/* GET MATCH TOTALS FOR THIS KEYSTROKE */
ms = getMatch(my_menu, c); /* get matches, first and following match */
/* JUST RETURN MATCHING CHOICE ON UNIQUE MATCH */
if(ms->matches == 1)
{
my_choice = ms->firstmatch;
}
/* GO TO NEXT MATCH ON MULTIPLE MATCHES */
else if (ms->matches > 1)
{
int nextChoice;

if(ms->followingmatch > 0) /*Try lower down the menu */
nextChoice = ms->followingmatch;
else /* otherwise highlight first match on menu */
nextChoice = ms->firstmatch;

/* DISPLAY THE RESULT */
set_current_item(my_menu, menu_items(my_menu)[nextChoice]);
refresh();
pos_menu_cursor(my_menu);
}
break;
} /* end of default case */
}
}

/* FREE ALL ALLOCATED RESOURCES */
free_item(my_items[0]);
free_item(my_items[1]);
free_menu(my_menu);

/* RETURN THE ZERO BASED NUMERIC USER CHOICE */
return(my_choice);
}

In the preceding, notice the following:

The Whole Program

Now, so you can try out this program for yourself, here's a pasteable listing of the keystroke responsive menu:

#include <curses.h>
#include <menu.h>
#include <stdlib.h> /* support for calloc() */
#include <ctype.h> /* support for toupper() */


typedef struct
{
int matches; /* number of menu items matching keystroke */
int firstmatch; /* first choice to match it */
int followingmatch; /* first matching choice after current choice */
} MATCHSTRUCT;

MATCHSTRUCT *getMatch(MENU *pmenu, char c)
{
/* DECLARE AND INITIALIZE VARIABLES */
const char * itemName;
int itemNo=0;
static MATCHSTRUCT matchstruct;
int currentChoiceNo = item_index(current_item(pmenu));

/* ZERO OUT TOTALS */
matchstruct.matches=0;
matchstruct.firstmatch=0;
matchstruct.followingmatch=0;

/* ITERATE THROUGH ALL ITEMS, STARTING WITH ITEM 0 */
/* THE FINAL ENTRY IN THE ITEM ARRAY IS ALWAYS A NULL POINTER */
while((itemName = item_name(menu_items(pmenu)[itemNo])) != NULL)
{
if(toupper(*itemName) == toupper(c)) /* match only regular keystrokes */
{
if(matchstruct.matches == 0) /* first match gets recorded as such */
{
matchstruct.firstmatch = itemNo;
}
matchstruct.matches++; /* update the match total */
if((itemNo > currentChoiceNo)) /* first match after current highlight */
{
matchstruct.followingmatch = itemNo;
currentChoiceNo = 32000; /* prevent further follow matches */
}
}
itemNo++; /* next item */
}
return(&matchstruct);
}

int runMenu(char *choices[])
{
ITEM **my_items; /* list of items on this menu */
int c; /* key pressed */
MENU *my_menu; /* the menu structure */
int n_choices; /* number of items on menu */
int ssChoice; /* subscript to run around the choices array */
int my_choice = -1; /* the zero based numeric user choice */


/* CALCULATE NUMBER OF MENU CHOICES */
for(n_choices=0; choices[n_choices]; n_choices++);

/* ALLOCATE ITEM ARRAY AND INDIVIDUAL ITEMS */
my_items = (ITEM **)calloc(n_choices + 1, sizeof(ITEM *));
for(ssChoice = 0; ssChoice < n_choices; ++ssChoice)
my_items[ssChoice] = new_item(choices[ssChoice], NULL);
my_items[n_choices] = (ITEM *)NULL;

/* CREATE THE MENU STRUCTURE AND DISPLAY IT */
my_menu = new_menu((ITEM **)my_items);
menu_opts_off(my_menu, O_NONCYCLIC);
post_menu(my_menu);
refresh();

/* HANDLE USER KEYSTROKES */
while(my_choice == -1)
{
c = getch();
switch(c)
{
case KEY_DOWN:
menu_driver(my_menu, REQ_DOWN_ITEM);
break;
case KEY_UP:
menu_driver(my_menu, REQ_UP_ITEM);
break;
case 10: /* Enter */
my_choice = item_index(current_item(my_menu));

/* RESET CURSOR IN CASE MORE SELECTION IS NECESSARY */
refresh();
pos_menu_cursor(my_menu);
break;
default:
{
MATCHSTRUCT * ms; /* matches, firstmatch, followingmatch */

/* MATCH ONLY PRINTABLE KEYSTROKES */
if((c <= ' ') || (c > '~')) break;

/* GET MATCH TOTALS FOR THIS KEYSTROKE */
ms = getMatch(my_menu, c); /* get matches, first and following match */
/* JUST RETURN MATCHING CHOICE ON UNIQUE MATCH */
if(ms->matches == 1)
{
my_choice = ms->firstmatch;
}
/* GO TO NEXT MATCH ON MULTIPLE MATCHES */
else if (ms->matches > 1)
{
int nextChoice;

if(ms->followingmatch > 0) /*Try lower down the menu */
nextChoice = ms->followingmatch;
else /* otherwise highlight first match on menu */
nextChoice = ms->firstmatch;

/* DISPLAY THE RESULT */
set_current_item(my_menu, menu_items(my_menu)[nextChoice]);
refresh();
pos_menu_cursor(my_menu);
}
break;
} /* end of default case */
}
}

/* FREE ALL ALLOCATED RESOURCES */
free_item(my_items[0]);
free_item(my_items[1]);
free_menu(my_menu);

/* RETURN THE ZERO BASED NUMERIC USER CHOICE */
return(my_choice);
}

int main()
{
char *choices[] = /* The menu choices */
{
"Two",
"One",
"Three",
"Four",
"Too many",
"Cancel",
NULL
};
int choiceno;

initscr(); /* start ncurses */
cbreak(); /* immediately acquire each keystroke */
noecho(); /* do not echo user keystrokes */
keypad(stdscr, TRUE); /* enable detection of function keys */

choiceno = runMenu(choices); /* acquire the user's choice */

endwin(); /* end ncurses */

printf("\n\nYou chose item %d, %s\n", choiceno, choices[choiceno]);
return(0);
}

Summary

We programmers must walk a mile in the user's shoes before writing our programs. Menus requiring the user to press the Enter key are easy to write, but double the number of menu keystrokes for the user. Worse yet are menus whose only navigation by the up and down keys. On large menus (10-15 items) this could triple the keystrokes.

A fast menu should execute a choice with a single keystroke, unless the choice is ambiguous due to multiple choices matching. In the case of multiple matching choices, the ideal user interface simply moves the highlight to the next matching choice, so once it arrives at the correct choice the user can press the Enter key to return the user's choice to the calling routine.

The ncurses library does not natively incorporate that algorithm. In fact, the ncurses library natively incorporates the Building Search Patterns algorithm, in which the search pattern is built up from user keystrokes. The Building Search Patterns algorithm is great for picklists and menus with huge numbers of choices, but not for the typical menu.

As a result, in this article we had to add our own code to keep track of unique and ambiguous choices. We did that with the MATCHSTRUCT structure and the getMatch() function and the default part of the switch statement in the runMenu() function.

In a real menu program, the character matching detection algorithm would be much more complex than the uppercase keystroke matching the uppercase first character of the choice text. A more typical algorithm might be something like this:
This logic is beyond the scope of this magazine, but if you want to see the logic, download the UMENU source code.
Steve Litt is the author of Rapid Learning: Secret Weapon of the Successful Technologist.   Steve can be reached at his email address.

Putting the Menu in its Own Window

By Steve Litt
Have you noticed that so far all menus have shown up in the upper left corner of the screen? What if that's not where you wanted it? What if you want the text below the menu to reappear once the user has chosen from the menu? What if you want a nice little box around the menu, or perhaps a nice title and maybe a few instructions?

This article answers all those questions by showing you how to associate a menu with Curses windows. In order to make things as simple as possible, the examples in this articles will NOT incorporate keyboard responsiveness, but instead will be a simple move and enter menu.

Key Concepts of Menu Windows

The MENU * structure always outputs at the upper left of its window. That means a couple things:

  1. If you place it on the main window (typically stdscr), it overwrites whatever's in the upper left corner.
  2. Even if you place it on its own window, it prints in the upper left, leaving no place for a decorative box, and probably overwriting any title that would be at the upper center.
The solution is to not only place the menu on its own window, but place that window on another window whose only purpose is to host the decorative box, the title, and possibly some prompts or instructions. I call the window containing the menu's user interface the User Interface Menu, and the window containing the border, title, and user interface window the Border Window. I think my terminology is clearer than that in the HOWTO, so let's go over some terminology:

Example graphic


Legend
Layout of a windowed menu      
Blue=Base Window

Dark red=Border window

 Light red=User Interface window

Here is a text description of these windows:

Some Menu Window Terminology
Term
Meaning
Border Window
This is a window whose only duty is to hold unchanging text and line graphics necessary to make the menu look good and communicate information like title and prompts to the user.

As a programmer, you size this window large enough to hold the User Interface Window, the outside line box, the title, and any prompts needed to guide the user. It obviously cannot be larger than the main screen (stdscr), whose size can be queried with getmaxyx(windowpointer,maxy,maxx). Moreover, to give the user context as to where he or she is in the app, this window should be smaller than the main screen.

This window is created with the newwin() function, making it totally independent of the main window. This is important, because this independence means that once the Border Window is destroyed, the text which it and its derived windows covered becomes visible again.

The following code creates a pointer to a Border Window called wBorder. This window is 22 lines high, 40 columns wide, and is positioned roughly in the center of an 80x25 screen. This would be appropriate for a menu of 10 items and several prompts, or a menu of 20 or more items (or a picklist) without prompts.

height=22; width=40; y=2; x=20;
wBorder = newwin(height, width, y, x);

The Border Window is associated with its menu via the set_menu_sub() function.

set_menu_win(MENU *, WINDOW *)
User Interface Window This is a window to contain the lines of the menu. These lines appear at the upper left of this window. This window is associated with the MENU * structure using:
set_menu_sub(MENU *, WINDOW *)

This window is created with size and position relative to the Border Window, so that the border and title properly surround the menu's lines. It is created with the derwin() function, because it is a window derived from the Border Window.

The following code positions the User Interface Window such that it is 2 characters smaller than the Border Window in both directions, and it places the User Interface Window such that it is centered inside the Border Window:

wUI = derwin(wBorder, height-2, width-2, 2, 2);
Menu Window
If you've read Pradeep Padala's outstanding NCURSES Programming HOWTO, you've seen this term. It's his term for what I call a Border Window. I imagine he named it this because this window is associated with the menu with the set_menu_win() function.

Personally, I think it's clearer to think of this window as a Border Window.
Menu Subwindow
Pradeep Padala's NCURSES Programming HOWTO, uses this term to describe what I call a User Interface Window. I imagine he named it this because this window is associated with the menu with the set_menu_sub() function.

Personally, I think it's clearer to think of this window as a User Interface Window.

The situation looks something like this:
Layout of a windowed menu

In the preceding layout, the base window is typically the size of the whole screen. The border window is a different color combination, and contains the title, border, and any prompt text ("Pick the best choice" in the preceding diagram). The user interface window contains the menu's user interface and nothing else. In the preceding diagram it's shown in a lighter red to emphasize the distinction, but in real life it's typically the same color combination as the border window. The user interface window cannot have its own border, for such border would be overwritten on the top left by the menu choices.

Window Menuing Pseudo Code

Before actually coding a menu on its own window, consider the major tasks going into creating the windowed menu:
Regarding the set_menu_fore() and set_menu_back() functions. These do not refer to the window's foreground and background. In fact, each sets a foreground AND background. set_menu_back() sets the foreground and background for the non-highlighted choices, so it should be the same color scheme as the Border Window. set_menu_fore() sets the foreground and background of the highlighted item, so it's typically the reverse of the Border Window.

The Code

The code to implement the preceding pseudocode is a little hairy, but if you think of it along the lines of the pseudocode it's actually pretty simple.

#include <curses.h>	/* Necessary for all Curses programs */
#include <menu.h> /* Gives you menuing capabilities */
#include <stdlib.h> /* Needed for calloc() */
#include <string.h> /* Needed for strlen() and friends */

#define WHITEONRED 1
#define WHITEONBLUE 2
#define WHITEONBLACK 3
#define BLACKONWHITE 4
#define REDONWHITE 5


void wCenterTitle(WINDOW *pwin, const char * title)
{
int x, maxy, maxx, stringsize;
getmaxyx(pwin, maxy, maxx);
stringsize = 4 + strlen(title);
x = (maxx - stringsize)/2;
mvwaddch(pwin, 0, x, ACS_RTEE);
waddch(pwin, ' ');
waddstr(pwin, title);
waddch(pwin, ' ');
waddch(pwin, ACS_LTEE);
}

void wclrscr(WINDOW * pwin)
{
int y, x, maxy, maxx;
getmaxyx(pwin, maxy, maxx);
for(y=0; y < maxy; y++)
for(x=0; x < maxx; x++)
mvwaddch(pwin, y, x, ' ');
}

bool initColors()
{
if(has_colors())
{
start_color();
init_pair(WHITEONRED, COLOR_WHITE, COLOR_RED);
init_pair(WHITEONBLUE, COLOR_WHITE, COLOR_BLUE);
init_pair(REDONWHITE, COLOR_RED, COLOR_WHITE);
return(true);
}
else
return(false);
}


int runMenu(
WINDOW *wParent,
int height,
int width,
int y,
int x,
char *choices[]
)
{
int c; /* key pressed */
ITEM **my_items; /* list of items on this menu */
MENU *my_menu; /* the menu structure */

WINDOW *wUI; /* window on which the user
interacts with the menu */
WINDOW *wBorder; /* window containing the wUI window
and the border and title */

int n_choices; /* number of items on menu */
int ssChoice; /* subscript to run around the choices array */
int my_choice = -1; /* the zero based numeric user choice */

/* CALCULATE NUMBER OF MENU CHOICES */
for(n_choices=0; choices[n_choices]; n_choices++);

/* ALLOCATE ITEM ARRAY AND INDIVIDUAL ITEMS */
my_items = (ITEM **)calloc(n_choices + 1, sizeof(ITEM *));
for(ssChoice = 0; ssChoice < n_choices; ++ssChoice)
my_items[ssChoice] = new_item(choices[ssChoice], NULL);
my_items[n_choices] = (ITEM *)NULL;

/* CREATE THE MENU STRUCTURE */
my_menu = new_menu((ITEM **)my_items);

/* PUT > TO THE LEFT OF HIGHLIGHTED ITEM */
set_menu_mark(my_menu, "> ");

/* SET UP WINDOW FOR MENU'S BORDER */
wBorder = newwin(height, width, y, x);
wattrset(wBorder, COLOR_PAIR(WHITEONRED) | WA_BOLD);
wclrscr(wBorder);
box(wBorder, 0, 0);
wCenterTitle(wBorder, "Choose one");

/* SET UP WINDOW FOR THE MENU'S USER INTERFACE */
wUI = derwin(wBorder, height-2, width-2, 2, 2);

/* ASSOCIATE THESE WINDOWS WITH THE MENU */
set_menu_win(my_menu, wBorder);
set_menu_sub(my_menu, wUI);

/* MATCH MENU'S COLORS TO THAT OF ITS WINDOWS */
set_menu_fore(my_menu, COLOR_PAIR(REDONWHITE));
set_menu_back(my_menu, COLOR_PAIR(WHITEONRED) | WA_BOLD);

/* SET UP AN ENVIRONMENT CONDUCIVE TO MENUING */
keypad(wUI, TRUE); /* enable detection of function keys */
noecho(); /* user keystrokes don't echo */
curs_set(0); /* make cursor invisible */

/* DISPLAY THE MENU */
post_menu(my_menu);

/* REFRESH THE BORDER WINDOW PRIOR TO ACCEPTING USER INTERACTION */
touchwin(wBorder);
wrefresh(wBorder);

/* HANDLE USER KEYSTROKES */
while(my_choice == -1)
{
touchwin(wUI); /* refresh prior to getch() */
wrefresh(wUI); /* refresh prior to getch() */
c = getch();
switch(c)
{
case KEY_DOWN:
menu_driver(my_menu, REQ_DOWN_ITEM);
break;
case KEY_UP:
menu_driver(my_menu, REQ_UP_ITEM);
break;
case 10: /* Enter */
my_choice = item_index(current_item(my_menu));

/* RESET CURSOR IN CASE MORE SELECTION IS NECESSARY */
pos_menu_cursor(my_menu);
break;
}
}

/* FREE ALL ALLOCATED MENU AND ITEM RESOURCES */
unpost_menu(my_menu);
for(ssChoice = 0; ssChoice < n_choices; ++ssChoice)
free_item(my_items[ssChoice]);
free_menu(my_menu);

/* DESTROY MENU WINDOW AND BORDER WINDOWS */
delwin(wUI);
delwin(wBorder);

/* UNDO MENU SPECIFIC ENVIRONMENT */
curs_set(1); /* make cursor visible again */

/* REPAINT THE CALLING SCREEN IN PREPARATION FOR RETURN */
touchwin(wParent);
wrefresh(wParent);

/* RETURN THE ZERO BASED NUMERIC USER CHOICE */
return(my_choice);
}


int main()
{
char *choices[] = /* The menu choices */
{
"One",
"Two",
"Three",
"Four",
"Cancel",
NULL
};
int choiceno;

initscr(); /* start ncurses */
cbreak(); /* immediately acquire each keystroke */
noecho(); /* do not echo user keystrokes */
keypad(stdscr, TRUE); /* enable detection of function keys */
initColors(); /* enable colors and initialize pairs */

/* SET UP AND PAINT STANDARD SCREEN */
wattrset(stdscr, COLOR_PAIR(WHITEONBLUE) | WA_BOLD);
wclrscr(stdscr);
mvwaddstr(stdscr, 10, 10, "Simple color menu");
touchwin(stdscr);
wrefresh(stdscr);

/* ACQUIRE THE USER'S CHOICE */
choiceno = runMenu(stdscr, 16, 40, 2, 20, choices);

mvwaddstr(stdscr, 22, 0, "Hit any key to finish==>");
touchwin(stdscr);
wrefresh(stdscr);
getch();

endwin(); /* end ncurses */

printf("\n\nYou chose item %d, %s\n", choiceno, choices[choiceno]);
return(0);
}



Summary

For a menu to be effective as a user interface element, it must be positionable, it must have its own color and border, and whatever it covers must be restorable. With Curses menus, all these needs are addressed by placing the menu on a User Interface Window, and placing the User Interface Window on a Border Menu. The Border Menu is created with newwin(), but the User Interface Window is created relative to the Border Menu using derwin(), which stands for derived window.

The Border Window's colors are set in usual way: wattrset(). Setting the user interface's colors accomplishes nothing. Instead, use set_menu_back() to set color scheme of unhighlighted items to the same as the Border Window. Use set_menu_fore() to set the color scheme of highlighted items to the reverse of that.

The way to associate the Border Window to the menu is with the set_menu_win() function. Associate the User Interface Window with the menu using the set_menu_sub() function.

The menu we've created might not highlight on a monochrome terminal. Therefore, it's important to have a secondary indication of which item is highlighted. This is done with the mark string, which is set using the
set_menu_mark() function. The default mark string is a hyphen immediately to the left of the item. To me, a "> " string is much more pleasing.

Whatever mark string you use, it will be ugly if you don't shut off the cursor with a call to cur_set(0). Naturally, immediately after the menu terminates you'll need to turn the cursor back on with cur_set(1).

Many Curses functions do not paint the screen or window until the screen or window is refreshed with touchwin() followed by wrefresh(). Make those two calls before every getch(), delay or increment to a progress indicator, on every window that might be changed.

Like every other C program, you must free everything you allocated. How to do this is shown toward the end of the runMenu() routine in this article's code.
Steve Litt is the author of Troubleshooting Techniques of the Successful Technologist.   Steve can be reached at his email address.

Menu Stuff We Haven't Covered

By Steve Litt
We've covered so much about menus, yet we've just scratched the surface. The Curses menu functionality allows for menus enabling multiple selections. Menus can scroll. Most menus need a "user declines to choose" choice, usually indicated by the user pressing the Esc key. Sometimes a menu should return something besides the subscript of the chosen item: For instance, the choice's text. Sometimes menu items have both a name text and a description. This would be natural in a database application. Sometimes menus have multiple columns in order to show more choices than lines of the screen, without scrolling. Sometimes menus need to directly execute a subroutine.

All these things are built into the Curses menu system. You know enough now that by looking in the NCURSES Programming HOWTO by Pradeep Padala, as well as /usr/include/ncurses/ncurses.h and /usr/include/ncurses/menu.h, you can find the necessary information.

Here's something that would be easily doable -- a file and directory selector. When run, it enables the user to navigate through a directory structure, choosing a file or directory, at which time that file or directory would be printed on stdout so that the calling program could use it. Such a program could be combined with the UMENU program to create a universal front end for almost any command, no matter how complex.

Next month's Linux Productivity Magazine will cover Curses-based forms and picklists.
Steve Litt is the author of the Universal Troubleshooting Process Courseware.   Steve can be reached at his email address.

Life After Windows: Positioning

Life After Windows is a regular Linux Productivity Magazine column, by Steve Litt, bringing you observations and tips subsequent to Troubleshooters.Com's Windows to Linux conversion.
By Steve Litt
If you watch TV, you've seen the commercial where three children make heartfelt statements about their future as adults. The commercial reminds you that your children will be grown up before you know it, and to enjoy them now. Is it a commercial for a family portrait photographer? For a backyard playground? For games you enjoy with your children? No, it's a commercial for a car.

What does a specific brand of car have to do with enjoying your children? Nothing.

But advertiser's research indicates that people are concerned about time flying by, and that some would actually believe that this particular car will help them have a better relationship with their children.

Advertisers call it "positioning". They have "positioned" their car as the one enabling you to enjoy your children :-)

IT folks are way too smart to fall for such  propaganda, aren't we?

Before answering that, consider the IT world's view of character based applications:
Is this view accurate? If not, where did it originate?

Is This View Accurate?

In assessing this view's accuracy, consider these facts about character based data processing and manipulation apps:
Now that I rebutted typical anti-character propaganda, let me tell you its advantages. Character based input is, by far, the fastest way to get information into a computer. No need of a mouse. No need to wait for a slow program (unless there are database problems). No need to restart the program or the operating system because the program bombs.

Character interfaces go anywhere. Without X, they run lightning fast on computers considered obsolete 7 years ago. With X, they consume miniscule resources while enabling the user to cut and paste between them and other programs. And they run quite well on hardware considered obsolete at the turn of the century.

Where Did the Anti-Character Viewpoint Originate?

So if character apps are so great, where did this anti-character viewpoint originate?

The marketing terms are "positioning", "positioning the competition", and "differentiation". My term is "anti-commoditization".

Once upon a time (early 1980's) there were only character based apps. All Microsoft's software was character based, including a character based Word. They had little competition. Compilers of the time were quirky and very expensive, so few programmers could get into the game. Then a guy named Philippe Kahn rocked Microsoft's world.

Kahn created a company called Borland, which created a lightning fast, works-every-time, capable-of-anything $39.00 compiler called Turbo Pascal. Overnight, every programmer with $39.00 and a 286 computer began pounding out apps on their kitchen  tables. Some apps were junk, and some rivaled Microsoft's offerings. Many were shareware with license prices in the $10.00 to  $30.00 range. Borland quickly followed up with Turbo C, and then Turbo C++, the first affordable C++ compiler. Millions of kitchen table programmers got into the game and cranked out software. The PKZip program was born around that time. Software became a commodity, depressing Microsoft's three digit prices.

Then the other shoe dropped. A company headed by a guy named Bruce Barrington created a RAD tool called Clarion. With Clarion, a programmer could whip out a very useful 5 table data app in a day, and then come back the next few days and customize it to the customer's heart's content. Why would anyone buy Microsoft Basic or Microsoft C.  There were actually numerous RAD tools of various capabilities, some even preceding Clarion. DBASE, RBASE, Magic PC, Paradox were all capable of quickly cranking out apps. Microsoft was in danger of becoming just another vendor, and an expensive one at that.

Microsoft's "solution" was to GUI-ize everything. Not just drawing programs and desktop publishing, but such mundane tasks as spreadsheets, configuration, and data. They packaged it with oh-wow, gee-whiz Windows 3.0 with its 3D buttons, and the world bought it hook, line and sinker. Even for simple data and configuration tasks.

With GUI in the mix, a great compiler was just a small piece of the puzzle. Now programmers needed expensive "tools", "frameworks", "environments", "drag and drop", and "RAD". Programming was set back 5 years. Once again, software development tools were quirky and very expensive, so few programmers could get into the game. Once again, Microsoft had little competition. Once again, software was not a commodity.

Microsoft did its job well. Kitchen Table Programming was never really the same. Yes, we all cranked out stuff in VB, but the resulting apps were glitchy, crashy, and hard to deploy. Some of us went on to Delphi, CBuilder and JBuilder, FoxPro and Powerbuilder. Those were good, but the development technique wasn't straightforward, and at least for me, some of the earlier CBuilder and JBuilder products were crashy. In the mid and late 1990's we all had fun becoming web designers until it became painfully obvious it was simple work rewarded with low pay. All too often, the really intense web work required development environments beyond the budget of a Kitchen Table Programmer.

We Kitchen Table Programmers almost scored again with Java. Zero cost, robust and stable, capable of creating nice apps. The problem was, the learning curve was hugely broad. And of course, Microsoft reacted with incompatable Visual J++ and then C#.

The Crisis and the Opportunity

If you're sitting in the United States reading this, you're in crisis. No matter how smart and productive you are, you cannot compete with someone who can live like a king for less than our country's minimum wage. Nor is retraining the answer -- retrain in what? All too soon those managers who made us train our replacements will themselves be offshored. I fear for our country.

But there's an opportunity here. Small businesses have never been able to afford custom programming. Now they can. I predict small business will be the last to accept foreign programmers. They don't have the connections.

If you can credibly present yourself as someone capable of creating useful custom apps in a day or two, and then servicing the client for a reasonable price, you can continue making good money. If the government nationalizes healthcare (and if the trend continues that's the only alternative to massively reduced life expectancies), working for small businesses will be a great alternative even for the head of a family.

When marketing one day custom apps, the priority is "does the job", not "looks pretty". Character apps do the job, reliably, without crashing, on a wide range of inexpensive systems. They're simple enough that mere mortals can create RAD tools for them -- maybe even Open Source RAD tools.

Meanwhile, Microsoft software, including their operating system, is becoming more and more expensive. This isn't fact lost on small businesses. Sooner or later they will accept Linux. And when they do, Curses is your path to quick and reliable character apps.

Positioning

So now you know how and why Microsoft managed to convince the world that crashy GUI apps are great, while convincing the world that lean, robust character apps are slow, underpowered, and yesterday's news. They employed the same techniques as the car company employed convincing us that their car is the way to enjoy our children.

We IT people need all the help we can in enjoying our children, what with our 80 hour workweeks increasing to 90 while we train our foreign replacements, and then upon our termination working double shifts at Burger King and Walmart to forestall foreclosure.

Don't let that be your future. Learn Curses, learn to make one day apps on Linux boxes, learn how to sell your services to small businesses. You owe it to your children.
Steve Litt is the founder and acting president of Greater Orlando Linux User Group (GoLUG).   Steve can be reached at his email address.

Letters to the Editor

All letters become the property of the publisher (Steve Litt), and may be edited for clarity or brevity. We especially welcome additions, clarifications, corrections or flames from vendors whose products have been reviewed in this magazine. We reserve the right to not publish letters we deem in bad taste (bad language, obscenity, hate, lewd, violence, etc.).


Submit letters to the editor to Steve Litt's email address, and be sure the subject reads "Letter to the Editor". We regret that we cannot return your letter, so please make a copy of it for future reference.

How to Submit an Article

We anticipate two to five articles per issue, with issues coming out monthly. We look for articles that pertain to the GNU/Linux or open source. This can be done as an essay, with humor, with a case study, or some other literary device. A Troubleshooting poem would be nice. Submissions may mention a specific product, but must be useful without the purchase of that product. Content must greatly overpower advertising. Submissions should be between 250 and 2000 words long.

Any article submitted to Linux Productivity Magazine must be licensed with the Open Publication License, which you can view at http://opencontent.org/openpub/. At your option you may elect the option to prohibit substantive modifications. However, in order to publish your article in Linux Productivity Magazine, you must decline the option to prohibit commercial use, because Linux Productivity Magazine is a commercial publication.

Obviously, you must be the copyright holder and must be legally able to so license the article. We do not currently pay for articles.

Troubleshooters.Com reserves the right to edit any submission for clarity or brevity, within the scope of the Open Publication License. If you elect to prohibit substantive modifications, we may elect to place editors notes outside of your material, or reject the submission, or send it back for modification. Any published article will include a two sentence description of the author, a hypertext link to his or her email, and a phone number if desired. Upon request, we will include a hypertext link, at the end of the magazine issue, to the author's website, providing that website meets the Troubleshooters.Com criteria for links and that the author's website first links to Troubleshooters.Com. Authors: please understand we can't place hyperlinks inside articles. If we did, only the first article would be read, and we can't place every article first.

Submissions should be emailed to Steve Litt's email address, with subject line Article Submission. The first paragraph of your message should read as follows (unless other arrangements are previously made in writing):

Copyright (c) 2003 by <your name>. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, version  Draft v1.0, 8 June 1999 (Available at http://www.troubleshooters.com/openpub04.txt/ (wordwrapped for readability at http://www.troubleshooters.com/openpub04_wrapped.txt). The latest version is presently available at  http://www.opencontent.org/openpub/).

Open Publication License Option A [ is | is not] elected, so this document [may | may not] be modified. Option B is not elected, so this material may be published for commercial purposes.

After that paragraph, write the title, text of the article, and a two sentence description of the author.

Why not Draft v1.0, 8 June 1999 OR LATER

The Open Publication License recommends using the word "or later" to describe the version of the license. That is unacceptable for Troubleshooting Professional Magazine because we do not know the provisions of that newer version, so it makes no sense to commit to it. We all hope later versions will be better, but there's always a chance that leadership will change. We cannot take the chance that the disclaimer of warranty will be dropped in a later version. 

Trademarks

All trademarks are the property of their respective owners. Troubleshooters.Com(R) is a registered trademark of Steve Litt.

URLs Mentioned in this Issue


_