Updated 2013-11-20 01:48:12 by RLE

2004-02-11 VI

I looked around for a curses package, so I could make my sysadmin scripts be more friendly. Here's a minimalist approach. A package which allows access to the bare bones. I think this should be enough to build any text UI, with everything else in Tcl. For Examples see:

See also curses.

See revision history at the end.
----
 /* curses.c
    A "minimalist" tcl package for interfacing to curses.  Goes into
    curses mode on load and automatically comes out at exit
    Venkat Iyer.  VI.  [email protected]
 
    Usage:
 
    curses init (no need to call, automatically done at load)
    curses end  (no need to call, automatically done at exit)
    curses attr <on/off> <standout/underline/reverse/blink/dim/bold/alt>
    curses move <row> <column> : move to screen position, 0 0 is top left
    curses puts  : print a string
    curses info  <rows/cols> : return the number of rows/cols in screen
    curses erase : clear the screen
    curses refresh : actually do refresh to physical screen
 
    Build: 
    gcc -I/usr/local/tcl/8.4.5/include -fPIC -c curses.c -o curses.o
 
    (something like this, which is on solaris7):
    ld -r curses.o -o curses.so -Bsymbolic /usr/lib/libcurses.a
    (on linux)
    gcc -shared curses.o -o curses.so /usr/lib/libcurses.a
 */
 
 #include "tcl.h"
 #include <curses.h>
 
 /*
  * Forward declarations for procedures defined later in this file:
  */
 
 static int cursesCmd _ANSI_ARGS_ ((ClientData dummy,
                                Tcl_Interp *interp, int objc, 
                                Tcl_Obj *CONST objvg));
 
 static int curses_start (void);
 static void curses_exit  (ClientData dummy); /* exit handler */
 
 /*
  *----------------------------------------------------------------------
  *
  * Curses_Init --
  *
  *     This procedure is the main initialisation point of the Curses
  *     extension.
  *
  * Results:
  *     Returns a standard Tcl completion code, and leaves an error
  *     message in the interp's result if an error occurs.  We're OK
  *      to init in a safe interpreter.  No file access done.
  *
  * Side effects:
  *     Adds a command to the Tcl interpreter.  Adds an exit handler
  *      and changes the screen into raw mode
  *
  *----------------------------------------------------------------------
  */
 
 
 int
 Curses_Init (interp)
     Tcl_Interp *interp;                /* Interpreter for application */
 {
     if (Tcl_InitStubs(interp, "8.4", 0) == NULL) {
        return TCL_ERROR;
     }
     if (Tcl_PkgRequire(interp, "Tcl", "8.4", 0) == NULL) {
        return TCL_ERROR;
     }
     if (Tcl_PkgProvide(interp, "curses", "0.8.0") == TCL_ERROR) {
         return TCL_ERROR;
     }
     
     Tcl_CreateObjCommand(interp, "curses", cursesCmd, 
                         (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
 
     curses_start();
     Tcl_CreateExitHandler(curses_exit,(ClientData) NULL);
     return TCL_OK;
 }
 
 /*
  * curses_exit -- exit handler for curses
  * Results: -- none
  * Side effects: gets out of curses by calling endwin
  */
 
 static void
 curses_exit (ClientData dummy)
 {
     endwin();
 }
 
 
 /* An error reporting routine for varargs results
  * Results : -- always TCL_ERROR, so we can just return
  * the value of this call
  * Side effects: Sets the result in the interpreter.
  */
 
 #define MAX_ERROR_SIZE   1024
 
 static int 
 setTclError TCL_VARARGS_DEF (
     Tcl_Interp *,
     i)
 {
     va_list argList;
     char buf[MAX_ERROR_SIZE];
     char *format;
     
     Tcl_Interp *interp = TCL_VARARGS_START(Tcl_Interp *, i, argList);
     format = va_arg(argList, char *);
     vsnprintf(buf, MAX_ERROR_SIZE, format, argList);
     buf[MAX_ERROR_SIZE-1] = '\0';
     Tcl_SetResult(interp, buf, TCL_VOLATILE);
     return TCL_ERROR;
 } 
 
 /*
  * curses_start -- init handler for curses. called on loading
  * Results: -- always TCL_OK
  * Side effects: gets into curses mode.
  */
 
 static int
 curses_start(void)
 {
     initscr();          /* will exit if there is an error */
     if (has_colors())   /* use colors if we have them */
         start_color();
     cbreak();
     noecho();
     nonl();
     intrflush(stdscr,FALSE);
     keypad(stdscr,TRUE);
     return TCL_OK;
 }
 
 /*
  * --------------------------------------------------------------- 
  * cursesCmd --
  *
  * Implmements the "curses" command.  Doesn't do colors yet
  * 
  * Results:
  *      A standard Tcl result. 
  *
  * Side effects:
  *      See the curses man page.  All side effects are inside the
  *      the library or on the screen!
  * 
  * Usage is listed at the top of this file
  */
     
 static int 
 cursesCmd (dummy, interp, objc, objv)
     ClientData dummy;
     Tcl_Interp *interp;
     int         objc;
     Tcl_Obj     *CONST objvg;
 {
     int index;
 
     static CONST char *optionStringsg = {
         "init", "end", "attr", "move", 
         "puts", "info", "erase", "refresh",
         NULL
     };
 
     enum options {
         CURSES_INIT, CURSES_END, CURSES_ATTR, CURSES_MOVE, 
         CURSES_PUTS, CURSES_INFO, CURSES_ERASE, CURSES_REFRESH
     };
 
     static CONST char *attrStringsg = {
         "standout", "underline", "reverse", 
         "blink", "dim", "bold", "alt", NULL
     };
 
     enum attrs {
         CURSES_A_STANDOUT, CURSES_A_UNDERLINE, CURSES_A_REVERSE,
         CURSES_A_BLINK,    CURSES_A_DIM,       CURSES_A_BOLD,
         CURSES_A_ALT
     };
 
     static CONST char *infoStringsg = {
         "cols", "lines", NULL
     };
 
     enum infos {
         CURSES_COLS,      CURSES_LINES
     };
 
     static CONST int attrValsg = {
         A_STANDOUT,   A_UNDERLINE,   A_REVERSE,
         A_BLINK,      A_DIM,         A_BOLD, 
         A_ALTCHARSET
     };
 
     if (objc < 2) {
         Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?");
         return TCL_ERROR;
     }
     
     if (Tcl_GetIndexFromObj(interp, objv[1], optionStrings, "option", 0,
             &index) != TCL_OK) 
         return TCL_ERROR;
 
     switch ((enum options) index) {
     case CURSES_INIT: {
         return curses_start();
     }
     case CURSES_END: {
         endwin();
         return TCL_OK;
     }
     case CURSES_ATTR: { 
         int on, index, attr;
 
         if (objc != 4) {
             Tcl_WrongNumArgs(interp, 2, objv, "boolean attribute");
             return TCL_ERROR;
         }
         if (Tcl_GetBooleanFromObj(interp, objv2, &on) != TCL_OK) 
             return TCL_ERROR;
         if (Tcl_GetIndexFromObj(interp, objv[3], attrStrings, 
                                 "attribute", 0, &index) != TCL_OK) 
             return TCL_ERROR;   /* perhaps allow an integer here? */
         attr = attrVals[index];
         if (on) {
             attron(attr);
         }  else {
             attroff(attr);
         }
         return TCL_OK;
     }
     case CURSES_MOVE: {
         int row, col;
         if (objc != 4) {
             Tcl_WrongNumArgs(interp, 2, objv, "row col");
             return TCL_ERROR;
         }
         if (Tcl_GetIntFromObj(interp, objv2, &row) != TCL_OK)
             return TCL_ERROR;
         if (Tcl_GetIntFromObj(interp, objv[3], &col) != TCL_OK)
             return TCL_ERROR;
         move(row, col);
         return TCL_OK;
     }
     case CURSES_PUTS: {
         if (objc != 3) {
             Tcl_WrongNumArgs(interp, 2, objv, "string");
             return TCL_ERROR;
         }
         addstr(Tcl_GetString(objv2));
         return TCL_OK;
     }
     case CURSES_INFO: {
         int index;
 
         if (objc != 3) {
             Tcl_WrongNumArgs(interp, 2, objv, "characteristic");
             return TCL_ERROR;
         }
         if (Tcl_GetIndexFromObj(interp, objv2, infoStrings,
                                 "characteristic", 0, &index) != TCL_OK)
             return TCL_ERROR;
         switch ((enum infos) index) {
         case CURSES_COLS: {
             Tcl_SetObjResult(interp,Tcl_NewIntObj(COLS));
             return TCL_OK;
         }
         case CURSES_LINES: {
             Tcl_SetObjResult(interp,Tcl_NewIntObj(LINES));
             return TCL_OK;
         }
         default: {
             return setTclError(interp, "Couldn't understand info "
                                "characteristic %d", index);
         }
         }
         return TCL_OK;
     }
     case CURSES_ERASE: {
         if (objc != 2) {
             Tcl_WrongNumArgs(interp, 2, objv, "");
             return TCL_ERROR;
         }
         erase();
         return TCL_OK;
     }
     case CURSES_REFRESH: {
         if (objc != 2) {
             Tcl_WrongNumArgs(interp, 2, objv, "");
             return TCL_ERROR;
         }
         refresh();
         return TCL_OK;
     }
     default: {
         return setTclError(interp, "Couldn't understand enum %d as "
                            "action type", index);
     }
     }
 }

The obvious missing things are color (and because this is curses and NOT ncurses, we don't have mouse and resize handling).

2004-02-12 VI add refresh and timeout (non-blocking) options to command. include linux build instructions

DKF: Arguably, just doing the package require should cause the initialization of the curses subsystem.

2004-02-13 VI It does. The pkgIndex.tcl is just one line:
 package ifneeded curses 0.7.2 [list load [file join $dir curses.so]]

And it also gets back to normal mode automatically on exit (an exit handler is installed during init).

2004-10-12 VI After a chat on the Tcl Chatroom found out that curses getch and curses timeout don't interact well with the Tcl event loop. Instead you set buffering to zero and non-blocking if required and do a read stdin 1. See the curses screen saver or curses digital clock for examples. So removed those two options to the curses command. So it's a little smaller now.

2004-11-07 Luis, I am going crazy with the code here presented because I don't know how to capture a character from the keyboard and act upon the character typed.

LV Some work has occurred over the years on cTk and perhaps another curses interface or two.

luis I just turn to python for curses programming (not knowing nothing about python) and I am impressed with the ease of use of python, so if you want to program in curses forget about tcl (except maybe por quick presentation of data with no user interaction) and use python instead.

2004-11-10 VI : Luis, please see Tcl Curses Menu for an example. And if you don't mind I'd like to move your code over to a separate page, as this is the page for getting the extension itself.

2004-11-11 12:14am Luis, no problem please erase it if you want, it is an ugly mess and I just put it here as an example, python is great but is strongly typed and the mysql extensions are not up to par with that of tcl, while on TCL everything is a string and it works great, I did take a look at the Tcl Curses Menu and it looks good to go, will try in more detail tomorrow, if it works fine I will definitely use this one even at the cost of two days of coding in python (somehow I have almost finish porting one big element of my application... which I thought would take 2 weeks...), also I am planning on creating a whole code base (similar to what you get on Foxplus, dbase) that is programming input fields, forms, scrolling lists, is minimalist curses tcl good enough for all this?, I am particularly intrigue by the use of arrow keys, F1 keys, control+key combinations, my guess is if you can get the raw codes from the keyboard there is no prob., by the way I am doing all this because my users got use to a screen display rather than using the more modern point and click interface, how's that for a weird case... no wonder windows has 90% of PC market, I mean you get use to what they serve you regardless...

2004-11-12 18:30pm Luis,hello again, I stumble into a little problem, is the arrow keys, binary scan [gets stdin 1] c k gives back the code of common letters but not so for arrow keys... for instance up arrow is code 65 (ascii decimal for A), I guess arrow keys have two bytes sequence, how could I change the line binary scan [gets stdin] c k for something that will give me back the correct two codes but still gives me the 0-255 ascii decimal codes in one pass of course, this is because I want to program input fields that can be edited with the arrow keys of the keyboard... I have been browsing thru binary scan man page but I haven't gotten this right... By the way your curses implementation works like a charm I am amazed it was this is easy to implement and that it will work as good as it does, now I am back on my project at full throttle and it looks good and it works very well, I highly recommend this extension to anyone out there wanting to program in curses in tcl. this surely gives TCL a new great functionality, with this tcl and mysql you have a dbase, foxplus, foxpro for terminals killer app, period. In fact it is so good that I wonder why isn't this more publicized this belongs in the official release of tcl/tk, is so obvious that it is weird it is not included and not well documented, for instance python includes a manual for stuff like this, because it is important for the language, is basic... In fact I want to contribute my experience in one of this pages how can I make a page in this wiki (like a little how to), to publicize this, I think it will help a lot of people out there... In my own experience there are a lot of programmers wanting to go to linux but coming from xbase backgrounds and not knowing how to because there is no dbase foxbase like app, with this you got that, much better... with vterminal you got this for windows... with a little library for input, menus (source inputs.tcl) you got a full replacement...

AJB - Luis, determining scan codes for key sequences will help you if you are using linux, or have access to a linux machine.

2004-11-15 VI A previous version of this extension did provide a curses getch command which returned canonicalized (in the curses sense) keys for function/arrow keys. If anyone's interested I can put it up on another page.

2004-11-23 luis everything is working fine thanks to the help I have received in this page, unfortunately I have not being able to advance at the pace I would wish because something got in the way at work (some RIP trouble which is new to me and have to sort it out somehow) anyway even with 1 hour a day I have advance a lot, what I have works fine and I will publish my own page (if i can find out how to) so others with this need can take advantage of this, what I intent to build and give a way is a xbase like alternative for linux (probably 2 or 3 people in the world would be interested by this at this point in computer history), never the less it is based on technology that is here to stay thanx to open source and that is up to date (tcl, mysql, curses, linux...), it will include everything you need to build forms, create lists, buttons, entry widgets for numbers for strings for dates, the thing will be useful basically for terminal displays (consoles) no graphics, my aim is for developers of xbase applications who might want to have an application that will consume little memory in a server available thru a terminal program and want to feed some information into the databases. I guess I better try to make a new page and put this there, just wanted to say thanx.

2004-12-1 Luis I have another problem I am Mexican so I use Spanish in my programs using the approach mentioned above to get keys from the keyboard gives me good results I get the correct Hex/dec/ascii codes for Spanish accented letters but when I display them on the screen with "curses put", I've seen the part where boxes are drawn and see You have to do curses attrib on alt, but that doesn't work either... I get strange characters that just don't match, however if I do a "put" the characters are ok, even with curses on, even dow the characters are on the range 0-254, for instance character ñ is 241 so it might not have to do with byte length, what can I do to make this work?, I am looking around for fix this I have posted this here as I think it relates to this important extension and also to look for help...