Wednesday, April 18, 2007

Basic terminal manipulation with C++ and ncurses

This one is a rather hastily written post. I frequent many online forums where newbies mob the lists and cause endless pain to the moderators with questions relating to some really evil non-standard ways of writing code that some compiler vendors of the Real Mode age popularised with their insipid IDEs. A large part of this concerns how to "clear the screen", "read a key press" and "position the cursor at some specified location" on the terminal screen.

I kind of figured out a plausible reason for this obsession with terminal manipulation. For some reason, the second program that some of these blokes have ever written after the customary "Hello, World!" was something like:

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

void main()
{
clrscr();
printf("Hello, World!\n");
getch();
}

Now I don't mean to be taken too seriously on this "second program" thing, but what I do mean is that too many youngsters have tried writing programs like the one above without much clue about where all they are going wrong.
If you feel revolting while reading code in non-monospace red font you are with me. But at this point, if you do not have bigger concerns about this code than the typesetting, then read on, cause you ought to have had, and having not had it, you need enlightenment. If you do have, read on all the same because you might still need the technique described below.

The above program is awful, for a number of reasons - all of which relate to a severe flouting of coding standards and in fact a severe breach of language standards by compiler which encourages and compiles such code. void main is of course wrong, but conio.h is not a standard header, nor are clrscr() and getch() standard functions.


Clearing the screen is a no-no

Apart from the syntactic issues, there are semantic issues in this piece of code as well. Know this, if you are writing a text console-based application, normally you will not want to clear the user's screen through your code. You never know what kind of terminal the user is using and what kind of data might be on display on this terminal. If the terminal is of the kind that can't be scrolled back, and there was sensitive data on display that the user somehow needed, and you happen to clear the screen just as you started your program - then at the least you have helped the user to a pretty bad experience, and at the most your company can be sued for losses or at least your product can earn a bad usability rating.


Keypress inputs are not cool

Blocking on a keypress with getch() would have been fine in the above example, had getch been standard. But even if it was standard, taking single character inputs from the user in the form of keypresses is a pretty bad idea, if the key pressed by the user matters to the program. There are some exceptions to it, but in general you would want the user to type exactly what she means, take a long hard look to make sure that this is indeed her intended input, and then confirm by hitting the Return key. Thereafter, the input is yours. But you don't want to process accidental key-presses as valid inputs. Barring exceptions, the general rule of thumb is, then:

1. Do not clear the user's screen on your own
2. Do not take important inputs as key presses.



The ncurses way

Even if we agree on the semantic rules of terminal manipulation above, we still have several questions unanswered.

1. Exactly what are those exceptional conditions when these services might be required?
2. If code is being written on *nix systems, or for that matter on Windows also, what is a portable way of clearing the screen, registering key-presses and positioning the cursor at arbitrary locations?

These are the questions, answers to which we explore in the rest of this article. Along the way, we will develop a C++ class which can be used on Unix terminals fairly portably to do all the terminal manipulation. It should work even on Windows, if the appropriate (free) libraries are available for linking.

I chose to answer the second question ahead of the first because at this point I have had enough words and not enough code. I guess it's true for you too. So how would such an implementation look, and how are we going to use it.

The UnixTerminal class and its use

Here is the header file containing the definition of a class called UnixTerminal. This class needs to be instantiated with just two arguments passed to its constructor - the input file descriptor and output file descriptor.


// file: UnixTerminal.hpp
#ifndef __UNIX_TERMINAL__
#define __UNIX_TERMINAL__

#include <boost/shared_ptr.hpp>

#include <string>
#include <exception>


class UnixTerminal
{
public:
enum mode_t { read = 0, write = 1 };
UnixTerminal( int in_filedes = STDIN_FILENO, int out_filedes = STDOUT_FILENO );

bool clear();
int keyPress( bool echo = true );
bool setCursorAt( int x, int y );

int getLines();
int getCols();

friend UnixTerminal& operator << ( UnixTerminal&, const std::string&amp; out );


friend UnixTerminal& operator >> ( UnixTerminal&, std::string&amp; in );

private:
int get_screen_measure( const char* measure_name );

struct TermData;
boost::shared_ptr in_, out_;
bool in_eof_;

struct PutcToTerm;
};


struct TermException : std::exception {
TermException ( const char * msg ) throw() : what_(msg)
{}

const char* what() throw()
{
return what_.c_str();
}

~TermException() throw()
{}

private:
std::string what_;
};

#endif /*__UNIX_TERMINAL__*/



A typical use of this class will be in code like the following:


#include "UnixTerminal.hpp"
#include <iostream>

int main()
{
try {
UnixTerminal ut;
std::string s1 = "Yonatan Rabin";

// print message
ut << s1;
// register key-press - without echoing (false)
char ch = ut.keyPress(false);
// clear screen
ut.clear();
std::cout << ch << std::endl;

// position cursor at the middle of the screen
if ( ! ut.setCursorAt( ut.getCols()/2, ut.getLines()/2 ) )
std::cerr << "Could not set cursor position" << std::endl;

// take line input into s2
std::string s2;
ut >> s2;

std::cout << s2 << ": " << s2.length() << std::endl;
}
catch (std::exception &obj) {
std::cout << obj.what();
}

return 0;
}

The ncurses-based implementation

This section will be ready soon.

0 Comments:

Post a Comment

<< Home