#ifdef RCSID
static char RCSid[] =
"$Header: d:/cvsroot/tads/TADS2/OSGEN.C,v 1.3 1999/07/11 00:46:30 MJRoberts Exp $";
#endif

/* 
 *   Copyright (c) 1990, 2002 Michael J. Roberts.  All Rights Reserved.
 *   
 *   Please see the accompanying license file, LICENSE.TXT, for information
 *   on using and copying this software.  
 */
/*
Name
  osgen3  - Operating System dependent functions, general implementation
            TADS 3 Version, with new "banner" interface
Function
  This module contains certain OS-dependent functions that are common
  between several systems.  Routines in this file are selectively enabled
  according to macros defined in os.h:

    USE_STDIO     - implement os_print, os_flush, os_gets with stdio functions
    USE_DOSEXT    - implement os_remext, os_defext using MSDOS-like filename
                    conventions
    USE_NULLINIT  - implement os_init and os_term as do-nothing routines
    USE_NULLPAUSE - implement os_expause as a do-nothing routine
    USE_EXPAUSE   - use an os_expause that prints a 'strike any key' message
                    and calls os_waitc
    USE_TIMERAND  - implement os_rand using localtime() as a seed
    USE_NULLSTAT  - use a do-nothing os_status function
    USE_NULLSCORE - use a do-nothing os_score function
    RUNTIME       - enable character-mode console implementation  
    USE_STATLINE  - implement os_status and os_score using character-mode
                    status line implementation
    USE_OVWCHK    - implements default saved file overwrite check
    USE_NULLSTYPE - use a dummy os_settype routine
    USE_NULL_SET_TITLE - use an empty os_set_title() implementation

    If USE_STDIO is defined, we'll implicitly define USE_STDIO_INPDLG.

    If USE_STATLINE is defined, certain subroutines must be provided for
    your platform that handle the character-mode console:
        ossclr - clears a portion of the screen
        ossdsp - displays text in a given color at a given location
        ossscr - scroll down (i.e., moves a block of screen up)
        ossscu - scroll up (i.e., moves a block of screen down)
        ossloc - locate cursor

    If USE_STATLINE is defined, certain sub-options can be enabled:
        USE_SCROLLBACK - include output buffer capture in console system
        USE_HISTORY    - include command editing and history in console system
Notes

Modified
  01/01/98 MJRoberts     - moved certain osgen.c routines to osnoui.c  
  04/24/93 JEras         - add os_locate() for locating tads-related files
  04/12/92 MJRoberts     - add os_strsc (string score) function
  03/26/92 MJRoberts     - add os_setcolor function
  09/26/91 MJRoberts     - os/2 user exit support
  09/04/91 MJRoberts     - stop reading resources if we find '$eof' resource
  08/28/91 MJRoberts     - debugger bug fix
  08/01/91 MJRoberts     - make runstat work correctly
  07/30/91 MJRoberts     - add debug active/inactive visual cue
  05/23/91 MJRoberts     - add user exit reader
  04/08/91 MJRoberts     - add full-screen debugging support
  03/10/91 MJRoberts     - integrate John's qa-scripter mods
  11/27/90 MJRoberts     - use time() not localtime() in os_rand; cast time_t
  11/15/90 MJRoberts     - created (split off from os.c)
*/

#define OSGEN_INIT
# include "os.h"
#undef OSGEN_INIT

#include "osgen.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <assert.h>

#include "run.h"

#if defined(TURBO) || defined(DJGPP)
#include "io.h"
#endif    

#include "lib.h"
#include "tio.h"

/*
 *   Flag: use "plain" mode.  If this is set, we'll use plain stdio output
 *   rather than our window-oriented display.  
 */
int os_f_plain = 0;

#ifdef RUNTIME
# ifdef USE_SCROLLBACK

/*
 *   Screen size variables.  The underlying system-specific "oss" code must
 *   initialize these during startup and must keep them up-to-date if the
 *   screen size ever changes.  
 */
int G_oss_screen_width = 80;
int G_oss_screen_height = 24;


# endif /* USE_SCROLLBACK */
#endif /* RUNTIME */

/*
 *   The special character codes for controlling color. 
 */

/* 
 *   set text attributes: this is followed by one byte giving the new
 *   attribute codes 
 */
#define OSGEN_ATTR            1

/* 
 *   Set text color: this is followed by two bytes giving the foreground and
 *   background colors as OSGEN_COLOR_xxx codes.
 *   
 *   Note well that the colors encoded in this escape sequence are
 *   OSGEN_COLOR_xxx values, not os_color_t values.  The latter require 32
 *   bits because they can store 24-bit RGB values plus some special
 *   parameter codes, while our internal OSGEN_COLOR_xxx values are only a
 *   byte each.  
 */
#define OSGEN_COLOR           2


/*
 *   If this port is to use the default saved file overwrite check, define
 *   USE_OVWCHK.  This routine tries to open the file; if successful, the
 *   file is closed and we ask the user if they're sure they want to overwrite
 *   the file.
 */
#ifdef USE_OVWCHK
int os_chkovw(char *filename)
{
    FILE *fp;
    
    if ((fp = fopen( filename, "r" )) != 0)
    {
        char buf[128];
        
        fclose(fp);
        os_printz("That file already exists.  Overwrite it? (y/n) >");
        os_gets((uchar *)buf, sizeof(buf));
        if (buf[0] != 'y' && buf[0] != 'Y')
            return 1;
    }
    return 0;
}
#endif /* USE_OVWCHK */

/* 
 *   non-stop mode does nothing in character-mode implementations, since the
 *   portable console layer handles MORE mode 
 */
void os_nonstop_mode(int flag)
{
}

/* ------------------------------------------------------------------------ */
/*
 *   Ports can implement os_flush and os_gets as calls to the stdio routines
 *   of the same name, and os_print and os_printz using the fputs() to
 *   stdout, by defining USE_STDIO.  These definitions can be used for any
 *   port for which the standard C run-time library is available.  
 */

#ifdef USE_STDIO

/* 
 *   os_printz works just like fputs() to stdout: we write a null-terminated
 *   string to the standard output.  
 */
void os_printz(const char *str)
{
    fputs(str, stdout);
}

/*
 *   os_puts works like fputs() to stdout, except that we are given a
 *   separate length, and the string might not be null-terminated 
 */
void os_print(const char *str, size_t len)
{
    printf("%.*s", (int)len, str);
}

/*
 *   os_flush forces output of anything buffered for standard output.  It
 *   is generally used prior to waiting for a key (so the normal flushing
 *   may not occur, as it does when asking for a line of input).  
 */
void os_flush(void)
{
    fflush(stdout);
}

/* 
 *   update the display - since we're using text mode, there's nothing we
 *   need to do 
 */
void os_update_display(void)
{
}

/*
 *   os_gets performs the same function as gets().  It should get a
 *   string from the keyboard, echoing it and allowing any editing
 *   appropriate to the system, and return the null-terminated string as
 *   the function's value.  The closing newline should NOT be included in
 *   the string.  
 */
uchar *os_gets(uchar *s, size_t bufl)
{
    /* make sure everything we've displayed so far is actually displayed */
    fflush(stdout);

    /* get a line of input from the standard input */
    return((uchar *)fgets((char *)s, bufl, stdin));
}

/*
 *   The default stdio implementation does not support reading a line of
 *   text with timeout.  
 */
int os_gets_timeout(unsigned char *buf, size_t bufl,
                    unsigned long timeout, int resume_editing)
{
    /* tell the caller this operation is not supported */
    return OS_EVT_NOTIMEOUT;
}

/* 
 *   since we don't support os_gets_timeout(), we don't need to do anything
 *   in the cancel routine
 */
void os_gets_cancel(int reset)
{
    /* os_gets_timeout doesn't do anything, so neither do we */
}

/*
 *   Get an event - stdio version.  This version does not accept a timeout
 *   value, and can only get a keystroke.  
 */
int os_get_event(unsigned long timeout, int use_timeout,
                 os_event_info_t *info)
{
    /* if there's a timeout, return an error indicating we don't allow it */
    if (use_timeout)
        return OS_EVT_NOTIMEOUT;

    /* get a key the normal way */
    info->key[0] = os_getc();

    /* if it's an extended key, get the other key */
    if (info->key[0] == 0)
    {
        /* get the extended key code */
        info->key[1] = os_getc();

        /* if it's EOF, return an EOF event rather than a key event */
        if (info->key[1] == CMD_EOF)
            return OS_EVT_EOF;
    }

    /* return the keyboard event */
    return OS_EVT_KEY;
}

#endif /* USE_STDIO */

/******************************************************************************
* Ports without any special initialization/termination requirements can define
* USE_NULLINIT to pick up the default definitions below.  These do nothing, so
* ports requiring special handling at startup and/or shutdown time must define
* their own versions of these routines.
******************************************************************************/

#ifdef USE_NULLINIT
/* os_init returns 0 for success, 1 for failure.  The arguments are &argc, the
*  address of the count of arguments to the program, and argv, the address of
*  an array of up to 10 pointers to those arguments.  For systems which don't
*  pass a standard command line (such as the Mac Finder), the arguments should
*  be read here using some alternate mechanism (an alert box, for instance),
*  and the values of argc and argv[] updated accordingly.  Note that a maximum
*  of 10 arguments are allowed to be stored in the argv[] array.  The command
*  line itself can be stored in buf, which is a buffer passed by the caller
*  guaranteed to be bufsiz bytes long.
*
*  Unix conventions are followed, so argc is 1 when no arguments are present.
*  The final argument is a prompt string which can be used to ask the user for
*  a command line; its use is not required, but may be desirable for producing
*  a relevant prompt message.  See the Mac implementation for a detailed
*  example of how this mechanism is used.
*/
int os_init(int *argc, char *argv[], const char *prompt,
            char *buf, int bufsiz)
{
    return 0;
}

/*
 *   uninitialize 
 */
void os_uninit(void)
{
}

/* 
 *   os_term should perform any necessary cleaning up, then terminate the
 *   program.  The int argument is a return code to be passed to the
 *   caller, generally 0 for success and other for failure.  
 */
void os_term(int rc)
{
    exit(rc);
}
#endif /* USE_NULLINIT */

/* ------------------------------------------------------------------------ */
/*   
 *   Ports can define USE_NULLPAUSE if no pause is required on exit.
 *   
 *   Ports needing an exit pause, and can simply print a message (with
 *   os_print) and wait for a key (with os_getc) can define USE_EXPAUSE.  
 */

#ifdef USE_NULLPAUSE
void os_expause(void)
{
    /* does nothing */
}
#endif /* USE_NULLPAUSE */

#ifdef USE_EXPAUSE
void os_expause(void)
{
    os_printz("(Strike any key to exit...)");
    os_flush();
    os_waitc();
}
#endif /* USE_EXPAUSE */


#ifdef USE_NULLSTAT
/*
 *   USE_NULLSTAT defines a do-nothing version of os_status.
 */
void os_status(int stat)
{
    /* ignore the new status */
}

int os_get_status()
{
    return 0;
}
#endif /* USE_NULLSTAT */

#ifdef USE_NULLSCORE
/*
 *   USE_NULLSCORE defines a do-nothing version of os_score.
 */
void os_score(int cur, int turncount)
{
    /* ignore the score information */
}

void os_strsc(const char *p)
{
    /* ignore */
}
#endif /* USE_NULLSCORE */

/* ------------------------------------------------------------------------ */
/*
 *   Scrollback 
 */

/* forward-declare the window control structure type */
typedef struct osgen_win_t osgen_win_t;

/*
 *   We can be compiled with or without scrollback.  The first version
 *   defines the scrollback implementation; the second just defines some
 *   dummy functions for the non-scrollback implementation.  
 */
# ifdef USE_SCROLLBACK

/* ------------------------------------------------------------------------ */
/* 
 *   Character color structure 
 */
typedef struct osgen_charcolor_t osgen_charcolor_t;
struct osgen_charcolor_t
{
    /* foreground color, as an OSGEN_COLOR_xxx value */
    char fg;

    /* background color, as an OSGEN_COLOR_xxx value */
    char bg;
};

/*
 *   Window control structure.  Each on-screen window is represented by one
 *   of these structures.  
 */
struct osgen_win_t
{
    /* type of the window - this is an OS_BANNER_TYPE_xxx code */
    int win_type;

    /* flags */
    unsigned int flags;

    /* next window in window list */
    osgen_win_t *nxt;

    /* 
     *   Parent window.  We take our display area out of the parent window's
     *   allocated space at the time we lay out this window.  
     */
    osgen_win_t *parent;

    /* head of list of children of this window */
    osgen_win_t *first_child;

    /* 
     *   The window's alignment type - this determines where the window goes
     *   relative to the main window.  This is an OS_BANNER_ALIGN_xxx
     *   alignment type code.  
     */
    int alignment;

    /* size type (as an OS_BANNER_SIZE_xxx value) */
    int size_type;

    /* 
     *   The window's size.  If size_type is OS_BANNER_SIZE_ABS, this is the
     *   size of the window in character cells.  If size_type is
     *   OS_BANNER_SIZE_PCT, this is given as a percentage of the full screen
     *   size.  
     */
    int size;

    /* upper-left corner position of window on screen */
    int winx;
    int winy;

    /* size of window on screen */
    size_t wid;
    size_t ht;

    /* 
     *   Cursor position (location of next output).  These are given in
     *   "document coordinates", which is to say that they're relative to
     *   the start of the text in the buffer.
     *   
     *   To translate to "window coordinates", simply subtract the scrolling
     *   offsets, which give the document coordinates of the first character
     *   displayed at the upper left corner of the window.
     *   
     *   To translate to absolute screen coordinates, first subtract the
     *   scrolling offsets to get window coordinates, then add the window's
     *   screen position (winx,winy).  
     */
    int x;
    int y;

    /* 
     *   maximum row and line where we've actually written any text (this
     *   can be used for purposes like setting horizontal scrollbar limits
     *   and sizing a window horizontally to its contents) 
     */
    int xmax;
    int ymax;

    /* scrolling offset of text displayed in window */
    int scrollx;
    int scrolly;

    /* 
     *   current text foreground and background colors (as OSGEN_COLOR_xxx
     *   values) 
     */
    char txtfg;
    char txtbg;

    /* current text attributes */
    int txtattr;

    /* window fill color (and oss color code for same) */
    char fillcolor;
    int oss_fillcolor;
};

/*
 *   Ordinary text stream window.  This is a subclass of the basic
 *   osgen_win_t window type, used when win_type is OS_BANNER_TYPE_TEXT.
 *   
 *   Ordinary text windows keep a circular buffer of scrollback text.  We
 *   optimize space by using escape codes embedded in the saved text stream
 *   to select colors and attributes.
 *   
 *   In the circular text buffer, each line ends with a null byte.  We keep
 *   an array of line-start pointers to make it fast and easy to find the
 *   first byte of a particular line.  The line-start array is also
 *   circular, and is organized in ascending order of row number.  
 */
typedef struct osgen_txtwin_t osgen_txtwin_t;
struct osgen_txtwin_t
{
    /* embed the base class */
    osgen_win_t base;

    /* text color and attributes at start of latest line */
    int solfg;
    int solbg;
    int solattr;
            
    /* window text buffer, and size of the buffer */
    char *txtbuf;
    size_t txtbufsiz;
    
    /* next free byte of window text buffer */
    char *txtfree;
    
    /* circular array of line-start pointers */
    char **line_ptr;
    size_t line_ptr_cnt;
    
    /* index of first line-start pointer in use */
    size_t first_line;
    
    /* number of lines of text stored in the buffer */
    size_t line_count;
};

/*
 *   Text Grid window.  This is a subclass of the basic window type,
 *   osgen_win_t, used when win_type is OS_BANNER_TYPE_TEXTGRID.
 *   
 *   A text grid window keeps a simple rectangular array of text, and a
 *   corresponding array of the color of each character.  The size of each
 *   array is at least as large as the window's actual area on the screen;
 *   when we resize the window, we'll reallocate the arrays at a larger size
 *   if the window has expanded beyond the stored size.  We don't keep any
 *   scrollback information in a text grid; we only keep enough to cover
 *   what's actually on the screen.  
 */
typedef struct osgen_gridwin_t osgen_gridwin_t;
struct osgen_gridwin_t
{
    /* embed the base class */
    osgen_win_t base;
    
    /* width and height of the text and color arrays */
    size_t grid_wid;
    size_t grid_ht;

    /* text array */
    char *grid_txt;
    
    /* color array */
    osgen_charcolor_t *grid_color;
};

/*
 *   Window flags 
 */

/* keep the cursor visible when adding text to the window */
#define OSGEN_AUTO_VSCROLL    0x0001

/* the window buffer is full and we're allowing nothing more to enter it */
#define OSGEN_FULL            0x0002

/* the window is in deferred-redraw mode */
#define OSGEN_DEFER_REDRAW    0x0004

/* 
 *   MORE mode in the banner.  Note that we keep track of this only so that
 *   we can indicate it on queries for the banner style; we count on the
 *   caller to handle the actual prompting for us.  
 */
#define OSGEN_MOREMODE        0x0008

/* child banner "strut" flags */
#define OSGEN_VSTRUT          0x0010
#define OSGEN_HSTRUT          0x0020

/* 
 *   The main text area window.  This window is special, because it's the
 *   root of the window tree.  
 */
static osfar_t osgen_txtwin_t *S_main_win = 0;

/* default status line window */
static osfar_t osgen_txtwin_t *S_status_win = 0;

/* default input/output window (for os_print, os_gets, etc) */
static osfar_t osgen_txtwin_t *S_default_win = 0;

/* current scrollback-mode window */
static osfar_t osgen_txtwin_t *S_sbmode_win = 0;

/* scrollback mode settings */
static osfar_t int S_sbmode_orig_scrolly;
static osfar_t int S_sbmode_orig_x;
static osfar_t char **S_sbmode_orig_last_line;
static osfar_t char *S_sbmode_orig_txtfree;

/* 
 *   flag: we're using a special cursor position; we use this to override our
 *   normal default cursor position 
 */
static osfar_t int S_special_cursor_pos = FALSE;
static osfar_t int S_special_cursor_x = 0;
static osfar_t int S_special_cursor_y = 0;

/* 
 *   Flag: deferred redraw required.  This indicates that something happened
 *   that requires redrawing the screen, but we didn't bother actually doing
 *   the redrawing immediately in case other things that would also require
 *   redrawing were to occur shortly. 
 */
static osfar_t int S_deferred_redraw = FALSE;

/*
 *   Input buffer state.  This information is defined statically because
 *   os_gets_timeout() can carry the information from invocation to
 *   invocation when input editing is interrupted by a tmieout.  
 */
static osfar_t char S_gets_internal_buf[256];       /* internal save buffer */
static osfar_t char *S_gets_buf = S_gets_internal_buf;  /* current save buf */
static osfar_t char *S_gets_buf_end = 0;             /* end of input buffer */
static osfar_t size_t S_gets_buf_siz = sizeof(S_gets_internal_buf); /* size */
static osfar_t int S_gets_ofs;       /* offset in buffer of insertion point */
static osfar_t char *S_gets_curhist;             /* current history pointer */
static osfar_t int S_gets_x, S_gets_y;             /* saved cursor position */

# ifdef USE_HISTORY
/* save buffer for line being edited before history recall began */
static osfar_t char S_hist_sav_internal[256]; 
static osfar_t char *S_hist_sav = S_hist_sav_internal;
static osfar_t size_t S_hist_sav_siz = sizeof(S_hist_sav_internal);
# endif /* USE_HISTORY */

/*
 *   Flag: input is already in progress.  When os_gets_timeout() returns
 *   with OS_EVT_TIMEOUT, it sets this flag to true.  os_gets_cancel() sets
 *   this flag to false.
 *   
 *   When os_gets_timeout() is called again, it checks this flag to see if
 *   the input session was cancelled; if not, the routine knows that the
 *   partially-edited input line is already displayed where it left off,
 *   because the display has not been modified since the interrupted call to
 *   os_gets_timeout() returned.  
 */
static osfar_t int S_gets_in_progress = FALSE;

/* forward declarations */
static void osssb_add_color_code(osgen_txtwin_t *win);
static void osgen_gridwin_clear(osgen_gridwin_t *win, size_t ofs, size_t len);
static void osgen_redraw_win(osgen_win_t *win);
static void osgen_scrdisp(osgen_win_t *win, int x, int y, int len);
static void osgen_gets_redraw_cmdline(void);

/*
 *   Delete a window 
 */
static void osgen_delete_win(osgen_win_t *win)
{
    osgen_win_t *prv;
    osgen_win_t *cur;
    osgen_txtwin_t *twin;
    osgen_gridwin_t *gwin;

    /* if we have a parent, remove ourselves from our parent's child list */
    if (win->parent != 0)
    {
        /* scan our parent's child list */
        for (prv = 0, cur = win->parent->first_child ;
             cur != 0 && cur != win ;
             prv = cur, cur = cur->nxt) ;

        /* if we found it, unlink it */
        if (cur != 0)
        {
            /* set the previous item's forward pointer */
            if (prv == 0)
                win->parent->first_child = win->nxt;
            else
                prv->nxt = win->nxt;
        }
    }

    /* 
     *   Remove the parent reference from each child of this window.  We're
     *   going to be deleted, so we can't keep references from our children
     *   to us.  
     */
    for (cur = win->first_child ; cur != 0 ; cur = cur->nxt)
        cur->parent = 0;

    /* delete the window according to its type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /* get the text window subclass data */
        twin = (osgen_txtwin_t *)win;

        /* delete its scrollback buffer, if it has one */
        if (twin->txtbuf != 0)
            osfree(twin->txtbuf);

        /* delete the line pointers */
        if (twin->line_ptr != 0)
            osfree(twin->line_ptr);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* get the grid window subclass data */
        gwin = (osgen_gridwin_t *)win;

        /* delete the character and color arrays */
        if (gwin->grid_txt != 0)
            osfree(gwin->grid_txt);
        if (gwin->grid_color != 0)
            osfree(gwin->grid_color);
        break;
    }

    /* delete the window itself */
    osfree(win);
}

/*
 *   Delete a window and all of its children 
 */
static void osgen_delete_win_tree(osgen_win_t *win)
{
    osgen_win_t *chi;
    osgen_win_t *nxt;

    /* delete the children first */
    for (chi = win->first_child ; chi != 0 ; chi = nxt)
    {
        /* remember the next one before we delete the current one */
        nxt = chi->nxt;

        /* delete this one */
        osgen_delete_win_tree(chi);
    }

    /* delete this window */
    osgen_delete_win(win);
}

/*
 *   Create a window and link it into our list.  We initialize the window
 *   and allocate its display buffer, but we do NOT set the window's size or
 *   position on the screen.  
 */
static osgen_win_t *osgen_create_win(int win_type, int where, void *other,
                                     osgen_win_t *parent)
{
    osgen_win_t *win;
    osgen_win_t *prv;
    osgen_win_t *cur;
    size_t struct_siz;

    /* figure the structure size based on the window type */
    switch(win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        struct_siz = sizeof(osgen_txtwin_t);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        struct_siz = sizeof(osgen_gridwin_t);
        break;

    default:
        /* unrecognized type - return failure */
        return 0;
    }

    /* create the window object */
    win = (osgen_win_t *)osmalloc(struct_siz);
    if (win == 0)
        return 0;

    /* remember the type */
    win->win_type = win_type;

    /* it's not in a window list yet */
    win->nxt = 0;

    /* initialize with default colors */
    win->txtfg = OSGEN_COLOR_TEXT;
    win->txtbg = OSGEN_COLOR_TRANSPARENT;
    win->txtattr = 0;
    win->fillcolor = OSGEN_COLOR_TEXTBG;

    /* cache the oss translation of the fill color */
    win->oss_fillcolor = ossgetcolor(OSGEN_COLOR_TEXT, OSGEN_COLOR_TEXTBG,
                                     0, 0);

    /* start at the upper left corner */
    win->x = 0;
    win->y = 0;
    win->scrollx = 0;
    win->scrolly = 0;

    /* 
     *   the window's position on screen will eventually be set by
     *   osgen_recalc_layout(), but initialize the position to a reasonable
     *   value for now in case anyone looks at it before then 
     */
    win->winx = 0;
    win->winy = 0;

    /* we haven't seen any text in the window yet */
    win->xmax = 0;
    win->ymax = 0;

    /* clear the flags */
    win->flags = 0;

    /* remember our parent */
    win->parent = parent;

    /* we have no children yet */
    win->first_child = 0;

    /* if there's a parent, insert the window into the parent's child list */
    if (parent != 0)
    {
        /* insert into the parent's child list at the proper point */
        switch(where)
        {
        case OS_BANNER_FIRST:
            /* link it at the head of the list */
            win->nxt = parent->first_child;
            parent->first_child = win;
            break;

        case OS_BANNER_LAST:
        default:
            /* find the end of the parent's list */
            for (cur = parent->first_child ; cur != 0 && cur->nxt != 0 ;
                 cur = cur->nxt) ;

            /* link it after the last element */
            win->nxt = 0;
            if (cur != 0)
                cur->nxt = win;
            else
                parent->first_child = win;

            /* done */
            break;

        case OS_BANNER_BEFORE:
        case OS_BANNER_AFTER:
            /* scan the parent's child list, looking for 'other' */
            for (prv = 0, cur = parent->first_child ;
                 cur != 0 && cur != other ;
                 prv = cur, cur = cur->nxt) ;

            /* 
             *   if we didn't find 'other', link the new window at the tail
             *   of the list by default; since 'prv' will be the last item if
             *   we didn't find 'other', we can simply set the link mode to
             *   'before' to link before the placeholder null at the end of
             *   the list 
             */
            if (cur == 0)
                where = OS_BANNER_BEFORE;

            /* if we're linking after 'cur', advance one position */
            if (where == OS_BANNER_AFTER)
            {
                prv = cur;
                cur = cur->nxt;
            }

            /* link before 'cur', which is right after 'prv' */
            win->nxt = cur;
            if (prv != 0)
                prv->nxt = win;
            else
                parent->first_child = win;

            /* done */
            break;
        }
    }

    /* return the new window */
    return win;
}

/*
 *   Create a text window 
 */
static osgen_txtwin_t *osgen_create_txtwin(int where, void *other,
                                           void *parent,
                                           unsigned int buf_size,
                                           unsigned int buf_lines)
{
    osgen_txtwin_t *win;
    
    /* create the base window */
    win = (osgen_txtwin_t *)osgen_create_win(OS_BANNER_TYPE_TEXT,
                                             where, other, parent);

    /* if that failed, give up now */
    if (win == 0)
        return 0;

    /* allocate the window's buffer */
    win->txtbufsiz = buf_size;
    win->txtbuf = (char *)osmalloc(buf_size);

    /* allocate the line starts */
    win->line_ptr_cnt = buf_lines;
    win->line_ptr = (char **)osmalloc(buf_lines * sizeof(char *));

    /* make sure we allocated everything properly */
    if (win->txtbuf == 0 || win->line_ptr == 0)
    {
        /* free anything we allocated */
        osgen_delete_win(&win->base);
        
        /* return failure */
        return 0;
    }

    /* set up the buffer free pointer */
    win->txtfree = win->txtbuf;

    /* start out with a single line in the buffer */
    win->line_count = 1;

    /* set up the first line start */
    win->first_line = 0;
    win->line_ptr[0] = win->txtbuf;

    /* initialize the start-of-line colors */
    win->solfg = win->base.txtfg;
    win->solbg = win->base.txtbg;
    win->solattr = win->base.txtattr;

    /* start the new first line with the current text color */
    osssb_add_color_code(win);

    /* return the new window */
    return win;
}

/*
 *   Create a text grid window 
 */
static osgen_gridwin_t *osgen_create_gridwin(int where, void *other,
                                             void *parent,
                                             int grid_wid, int grid_ht)
{
    osgen_gridwin_t *win;

    /* create the base window */
    win = (osgen_gridwin_t *)osgen_create_win(OS_BANNER_TYPE_TEXTGRID,
                                              where, other, parent);

    /* if that failed, give up now */
    if (win == 0)
        return 0;
    
    /* allocate the grid to the requested size */
    win->grid_wid = grid_wid;
    win->grid_ht = grid_ht;
    win->grid_txt = (char *)osmalloc(grid_wid * grid_ht);
    win->grid_color = (osgen_charcolor_t *)osmalloc(
        grid_wid * grid_ht * sizeof(osgen_charcolor_t));

    /* if we failed to allocate, delete the window and abort */
    if (win->grid_txt == 0 || win->grid_color == 0)
    {
        /* free anything we allocated */
        osgen_delete_win(&win->base);
        
        /* return failure */
        return 0;
    }

    /* clear the grid text and color arrays */
    osgen_gridwin_clear(win, 0, win->grid_wid * win->grid_ht);

    /* return the new window */
    return win;
}

/*
 *   Clear a window 
 */
static void osgen_clear_win(osgen_win_t *win)
{
    osgen_txtwin_t *twin;
    osgen_gridwin_t *gwin;

    /* check the type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /* get the text window subclass data */
        twin = (osgen_txtwin_t *)win;

        /* reset the window's scrollback buffer */
        twin->txtfree = twin->txtbuf;
        twin->line_count = 1;
        twin->first_line = 0;
        twin->line_ptr[0] = twin->txtbuf;

        /* start the new first line with the current text color */
        osssb_add_color_code(twin);

        /* done */
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* get the grid window subclass data */
        gwin = (osgen_gridwin_t *)win;

        /* clear the entire grid */
        osgen_gridwin_clear(gwin, 0, gwin->grid_wid * gwin->grid_ht);

        /* done */
        break;
    }

    /* put the cursor at the top left of the window */
    win->x = 0;
    win->y = 0;

    /* set the scrolling position to the top of the window */
    win->scrollx = 0;
    win->scrolly = 0;

    /* the content size is now zero */
    win->xmax = 0;
    win->ymax = 0;

    /* if the buffer was full, it's full no longer */
    win->flags &= ~OSGEN_FULL;

    /*
     *   If this window is not auto-scrolling, don't actually clear it
     *   visually right now, but just set a deferred-redraw on the window;
     *   clearing a window that doesn't automatically scroll usually means
     *   that we're updating some live status information, so we use deferred
     *   drawing on these windows to reduce flicker during updates.  If the
     *   window automatically scrolls, then the caller presumably is
     *   displaying more sizable data, and thus would want the user to see
     *   the text output as it occurs.
     *   
     *   Note that both approaches (deferred vs immediate redraw) yield the
     *   same results in the end, because we always stop deferring redraws
     *   when we stop to ask for input or simply pause for a time delay; the
     *   only difference is that deferred redrawing reduces flicker by
     *   gathering all the updates into a single operation, while immediate
     *   update might flicker more but shows individual bits of text output
     *   as they occur.  
     */
    if (!(win->flags & OSGEN_AUTO_VSCROLL))
    {
        /* defer redrawing until we need to */
        win->flags |= OSGEN_DEFER_REDRAW;
    }
    else
    {
        /* clear the window's on-screen area with its fill color */
        ossclr(win->winy, win->winx,
               win->winy + win->ht - 1, win->winx + win->wid - 1,
               win->oss_fillcolor);
    }
}

/*
 *   Display text in a window, all in a single color, truncating the display
 *   if we start before the left edge of the window or go past the right
 *   edge of the window.  The x,y coordinates are given in window
 *   coordinates.  
 */
static void osgen_disp_trunc(osgen_win_t *win, int y, int x, int oss_color,
                             const char *p)
{
    char buf[64];
    size_t chars_rem;
    size_t wid_rem;

    /* if we have deferred redrawing pending, don't bother drawing now */
    if (S_deferred_redraw || (win->flags & OSGEN_DEFER_REDRAW) != 0)
        return;

    /* get the number of characters to be displayed */
    chars_rem = strlen(p);

    /* calculate the amount of space we have to work with on the screen */
    wid_rem = win->wid - (x >= 0 ? x : 0);

    /* make sure at least some of the text overlaps the window */
    if (y < 0 || y >= (int)win->ht
        || x + (int)chars_rem <= 0 || x >= (int)win->wid)
        return;

    /* 
     *   if we're starting to the left of the window, skip characters up to
     *   the first character visible in the window 
     */
    if (x < 0)
    {
        /* 
         *   get the number of characters to skip - this is simply the
         *   negative of the x position, since x position zero is the first
         *   column to display 
         */
        x = -x;

        /* 
         *   if we don't have enough characters to reach the left edge of
         *   the window, we have nothing to do 
         */
        if (chars_rem <= (size_t)x)
            return;

        /* skip the desired number of characters */
        chars_rem -= x;
        p += x;

        /* we've skipped up to column zero, so proceed from there */
        x = 0;
    }

    /* if the text entirely fits, display it and we're done */
    if (chars_rem < wid_rem)
    {
        /* display the entire string */
        ossdsp(win->winy + y, win->winx + x, oss_color, p);

        /* done */
        return;
    }

    /* 
     *   we have too much to display, so display as much as will fit - keep
     *   going until we run out of space for the display (we know we'll run
     *   out of space before we run out of text, because we know we have too
     *   much text to fit) 
     */
    while (wid_rem != 0)
    {
        size_t cur;

        /* display as much as will fit, up to our buffer length */
        cur = wid_rem;
        if (cur > sizeof(buf) - 1)
            cur = sizeof(buf) - 1;

        /* copy this portion to our buffer and null-terminate it */
        memcpy(buf, p, cur);
        buf[cur] = '\0';

        /* display it */
        ossdsp(win->winy + y, win->winx + x, oss_color, buf);

        /* advance our screen position */
        x += cur;

        /* advance past the source material we just displayed */
        p += cur;

        /* deduct the on-screen space we consumed from the remaining space */
        wid_rem -= cur;
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Initialize the scrollback buffer.  This initializes the main text area
 *   window.
 *   
 *   The oss code should call this during initialization to set up the
 *   osgen3-layer display management; note that this routine must be called
 *   from the oss code AFTER the screen size has been determined and stored
 *   in the global variables G_oss_screen_width and G_oss_screen_height.  
 */
void osssbini(unsigned int size)
{
    osgen_txtwin_t *win;

    /* allocate our main window if we're not in 'plain' mode */
    if (!os_f_plain)
    {
        /* 
         *   Initialize the main window.  The main window has no parent,
         *   since it's the root of the window tree.  
         */
        S_main_win = win = osgen_create_txtwin(
            OS_BANNER_FIRST, 0, 0, size, size/40);
        if (win == 0)
        {
            /* show a message and give up */
            printf("There is not enough memory to run this program.\n");
            exit(1);
        }
        
        /* make this the default window */
        S_default_win = win;
        
        /* scroll to keep the cursor visible when writing to this window */
        win->base.flags |= OSGEN_AUTO_VSCROLL;
        
        /* initially give the window the entire screen */
        win->base.winy = 0;
        win->base.winx = 0;
        win->base.wid = G_oss_screen_width;
        win->base.ht = G_oss_screen_height;
        
        /* set the initial page size */
        G_os_linewidth = win->base.wid;
        G_os_pagelength = (win->base.ht > 2
                           ? win->base.ht - 2
                           : win->base.ht > 1
                           ? win->base.ht - 1 : win->base.ht);

        /* set the initial color scheme to the default text color */
        os_set_text_color(OS_COLOR_P_TEXT, OS_COLOR_P_TRANSPARENT);
        os_set_screen_color(OS_COLOR_P_TEXTBG);

        /* clear the window */
        osgen_clear_win(&win->base);
    }
    else
    {
        /* use the entire available display */
        G_os_linewidth = G_oss_screen_width;
        G_os_pagelength = G_oss_screen_height - 2;
    }
}

/*
 *   Delete the scrollback buffer.  The oss code should call this during
 *   program termination to free memory allocated by the osgen3 layer.  
 */
void osssbdel(void)
{
    /* delete the main window and its children */
    if (S_main_win != 0)
        osgen_delete_win_tree(&S_main_win->base);
}

/* ------------------------------------------------------------------------ */
/*
 *   Scrollback operations.  Scrollback applies only to ordinary text
 *   windows.  
 */

/*
 *   advance a pointer in a scrollback buffer 
 */
static char *ossadvsp(osgen_txtwin_t *win, char *p)
{
    /* move to the next byte */
    ++p;

    /* if we've passed the end of the buffer, wrap to the beginning */
    if (p >= win->txtbuf + win->txtbufsiz)
        p = win->txtbuf;

    /* return the pointer */
    return p;
}

/*
 *   decrement a scrollback buffer pointer 
 */
static char *ossdecsp(osgen_txtwin_t *win, char *p)
{
    /* if we're at the start of the buffer, wrap to just past the end */
    if (p == win->txtbuf)
        p = win->txtbuf + win->txtbufsiz;

    /* move to the previous byte */
    --p;

    /* return the pointer */
    return p;
}

/*
 *   Get the line pointer for the given line 
 */
static char **osgen_get_line_ptr(osgen_txtwin_t *win, int y)
{
    size_t idx;

    /* if the line doesn't exist, return null */
    if (y < 0 || (size_t)y >= win->line_count)
        return 0;

    /* add the line number to the base index to get the linear index */
    idx = y + win->first_line;

    /* wrap back to the start of the array if necessary */
    if (idx >= win->line_ptr_cnt)
        idx -= win->line_ptr_cnt;

    /* return the line at this index */
    return &win->line_ptr[idx];
}

/*
 *   Get a pointer to the text of the line in the given window at the given
 *   document y coordinate.  
 */
static char *osgen_get_line(osgen_txtwin_t *win, int y)
{
    char **line_ptr;

    /* get the line pointer for the given line */
    line_ptr = osgen_get_line_ptr(win, y);

    /* return the text it points to, or null if there's no such line */
    return (line_ptr != 0 ? *line_ptr : 0);
}

/*
 *   Add a byte to the scrollback buffer.  Returns true if the byte was
 *   successfully added, false if not.  
 */
static int osssb_add_byte(osgen_txtwin_t *win, char c)
{
    char *nxtfree;

    /* if the window is full, indicate failure */
    if ((win->base.flags & OSGEN_FULL) != 0)
        return FALSE;

    /* 
     *   If there's no room, and we're not in auto-vscroll mode, simply
     *   discard the text.  A window that is not in auto-vscroll mode is
     *   designed to always show the oldest text, so if we overflow the
     *   buffer, then we want to start discarding the newest test.  
     */
    nxtfree = ossadvsp(win, win->txtfree);
    if (nxtfree == win->line_ptr[win->first_line]
        && !(win->base.flags & OSGEN_AUTO_VSCROLL))
    {
        /* 
         *   The buffer is full, and we're not in auto-vscroll mode -
         *   discard the new text.  Before we do, set the current last byte
         *   to null, to terminate the current line at this point.  
         */
        *win->txtfree = '\0';

        /* set the 'full' flag */
        win->base.flags |= OSGEN_FULL;

        /* indicate failure */
        return FALSE;
    }

    /* add the character to the buffer */
    *win->txtfree = c;

    /* advance the free pointer */
    win->txtfree = nxtfree;

    /* 
     *   if the free pointer has just collided with the start of the oldest
     *   line, delete the oldest line 
     */
    if (win->txtfree == win->line_ptr[win->first_line])
    {
        /* delete the oldest line to make room for the new data */
        win->first_line++;
        if (win->first_line >= win->line_ptr_cnt)
            win->first_line = 0;

        /* we have one less line in the buffer */
        win->line_count--;

        /* adjust our document cooordinates for the loss of a line */
        win->base.y--;

        /* 
         *   Adjust the scrolling position for the loss of a line.  If the
         *   window is displaying the line that we just deleted (i.e., the
         *   window's vertical scroll offset is zero), we must redraw the
         *   window; otherwise, the change won't affect the display, so we
         *   can merely adjust the scrolling offset.  
         */
        if (win->base.scrolly == 0)
            win->base.flags |= OSGEN_DEFER_REDRAW;
        else
            win->base.scrolly--;
    }

    /* we successfully added the byte */
    return TRUE;
}

/*
 *   Add a given number of bytes.  The bytes will be added as a unit - if we
 *   can't add all of the bytes, we won't add any of them.  
 */
static int osssb_add_bytes(osgen_txtwin_t *win, const char *p, size_t len)
{
    char *orig_free;

    /* note the original free pointer */
    orig_free = win->txtfree;

    /* add the bytes */
    for ( ; len != 0 ; --len)
    {
        /* try adding this byte */
        if (!osssb_add_byte(win, *p++))
        {
            /* 
             *   failure - forget everything we've added by resetting the
             *   free pointer to its original value on entry 
             */
            win->txtfree = orig_free;

            /* indicate failure */
            return FALSE;
        }
    }

    /* we added all of the bytes successfully */
    return TRUE;
}

/*
 *   Add a "safety" null byte.  This adds a null byte at the end of the
 *   buffer, in case we need to inspect the current line before we add any
 *   more text.  
 */
static void osssb_add_safety_null(osgen_txtwin_t *win)
{
    /* add the null */
    if (osssb_add_byte(win, '\0'))
    {
        /* 
         *   we added the null - decrement the free pointer so we overwrite
         *   the null byte if we add any more text to the current line 
         */
        win->txtfree = ossdecsp(win, win->txtfree);
    }
}

/*
 *   Add an appropriate color code to the scrollback buffer to yield the
 *   current color setting.  
 */
static void osssb_add_color_code(osgen_txtwin_t *win)
{
    char buf[10];

    /* if we have any attributes, add an attribute code */
    if (win->base.txtattr != 0)
    {
        /* add the attribute code sequence */
        buf[0] = OSGEN_ATTR;
        buf[1] = (char)win->base.txtattr;
        osssb_add_bytes(win, buf, 2);
    }

    /* if we're using the plain text color, add a color code */
    if (win->base.txtfg != OSGEN_COLOR_TEXT
        || win->base.txtbg != OSGEN_COLOR_TRANSPARENT)
    {
        /* add the explicit color code */
        buf[0] = OSGEN_COLOR;
        buf[1] = win->base.txtfg;
        buf[2] = win->base.txtbg;
        osssb_add_bytes(win, buf, 3);
    }

    /* add a safety null terminator */
    osssb_add_safety_null(win);
}

/*
 *   Start a new line in the scrollback buffer.  Returns true on success,
 *   false if there's no space to add the newline character.  
 */
static int osssb_new_line(osgen_txtwin_t *win)
{
    char **line_ptr;

    /* add a null byte to mark the end of the current line */
    if (!osssb_add_byte(win, '\0'))
        return FALSE;

    /* if the window is full, indicate failure */
    if ((win->base.flags & OSGEN_FULL) != 0)
        return FALSE;

    /* 
     *   If the line buffer is full, and this window doesn't automatically
     *   scroll vertically, drop the new text.  When we're not in
     *   auto-vscroll mode, we drop text from the end of the buffer rather
     *   than from the beginning.  
     */
    if (win->line_count + 1 > win->line_ptr_cnt
        && !(win->base.flags & OSGEN_AUTO_VSCROLL))
    {
        /* set the 'full' flag */
        win->base.flags |= OSGEN_FULL;

        /* indicate failure */
        return FALSE;
    }

    /* move to the left column */
    win->base.x = 0;

    /* add a line start */
    if (win->line_count >= win->line_ptr_cnt)
    {
        /* forget the original first line */
        win->first_line++;
        if (win->first_line == win->line_ptr_cnt)
            win->first_line = 0;

        /* 
         *   Since we took away one line and we're adding another, the total
         *   line count and the document 'y' position aren't changing - but
         *   the line at the document 'y' position now belongs to the new
         *   last line.
         *   
         *   Adjust the scrolling position for the loss of a line at the
         *   start of the buffer.  This doesn't change what we're displaying
         *   on the screen, because we're simply reducing the amount of text
         *   in the buffer before the top of the screen, so we must
         *   compensate by decrementing the document coordinate of the top
         *   line in the window.  However, if the line we're taking away is
         *   showing in the window, then we do need to redraw the contents,
         *   because the change will be visible; set the deferred redraw
         *   flag for the window in this case so that we eventually redraw
         *   the whole thing.  
         */
        if (win->base.scrolly == 0)
            win->base.flags |= OSGEN_DEFER_REDRAW;
        else
            win->base.scrolly--;
    }
    else
    {
        /* count the new line */
        win->line_count++;

        /* note the highest output 'y' position if this is it */
        if (win->base.y > win->base.ymax)
            win->base.ymax = win->base.y;

        /* increase the document y position for the new line */
        win->base.y++;
    }

    /* get the line pointer for the new last line */
    line_ptr = osgen_get_line_ptr(win, win->base.y);

    /* 
     *   set the new line pointer to point to the first free byte of the
     *   buffer 
     */
    *line_ptr = win->txtfree;

    /* add a color code to the start of the new line, if necessary */
    osssb_add_color_code(win);

    /* remember the text color at the start of this line */
    win->solfg = win->base.txtfg;
    win->solbg = win->base.txtbg;
    win->solattr = win->base.txtattr;

    /* add a safety null terminator */
    osssb_add_safety_null(win);

    /* success */
    return TRUE;
}

/*
 *   Scroll the window forward by the given number of lines 
 */
static void osgen_scroll_win_fwd(osgen_txtwin_t *win, size_t lines)
{
    /* limit the scrolling to the available number of lines */
    if (lines > win->line_count - win->base.scrolly - win->base.ht)
        lines = win->line_count - win->base.scrolly - win->base.ht;

    /* 
     *   If we're scrolling more than a few lines, or more than the entire
     *   window's height, just redraw the entire window.  If it's a small
     *   number of lines, scroll the screen one line at a time, so that we
     *   don't have to redraw as much.  
     */
    if (lines > 3 || (int)lines >= win->base.ht)
    {
        /* adjust the scroll position */
        win->base.scrolly += lines;

        /* redraw the entire window */
        osgen_redraw_win(&win->base);
    }
    else
    {
        /* scroll one line at a time */
        for ( ; lines != 0 ; --lines)
        {
            /* scroll the window's on-screen area one line */
            ossscr(win->base.winy, win->base.winx,
                   win->base.winy + win->base.ht - 1,
                   win->base.winx + win->base.wid - 1,
                   win->base.oss_fillcolor);

            /* adjust the scroll position */
            win->base.scrolly++;

            /* redraw the bottom line */
            osgen_scrdisp(&win->base, 0, win->base.ht - 1, win->base.wid);
        }
    }
}

/*
 *   Scroll the window back by the given number of lines 
 */
static void osgen_scroll_win_back(osgen_txtwin_t *win, int lines)
{
    /* limit scrolling to the available number of lines above the window */
    if (lines > win->base.scrolly)
        lines = win->base.scrolly;

    /* 
     *   If we're scrolling more than a few lines, or by the full window
     *   height or more, just redraw the entire window.  Otherwise, scroll a
     *   line at a time to minimize redrawing.  
     */
    if (lines > 3)
    {
        /* adjust the scroll position */
        win->base.scrolly -= lines;

        /* redraw the entire window */
        osgen_redraw_win(&win->base);
    }
    else
    {
        /* scroll up by one line at a time */
        for ( ; lines != 0 ; --lines)
        {
            /* scroll the window's on-screen area one line */
            ossscu(win->base.winy, win->base.winx,
                   win->base.winy + win->base.ht - 1,
                   win->base.winx + win->base.wid - 1,
                   win->base.oss_fillcolor);

            /* adjust the scroll position */
            win->base.scrolly--;

            /* redraw the top line */
            osgen_scrdisp(&win->base, 0, 0, win->base.wid);
        }
    }
}

/*
 *   If a window is in auto-vscroll mode, bring the cursor into view in the
 *   window. 
 */
static void osgen_auto_vscroll(osgen_txtwin_t *win)
{
    /* if the window isn't in auto-vscroll mode, ignore this */
    if (!(win->base.flags & OSGEN_AUTO_VSCROLL))
        return;
    
    /* 
     *   scroll the window forward if the cursor is outside the visible area
     *   of the window 
     */
    if (win->base.y >= win->base.scrolly + (int)win->base.ht)
        osgen_scroll_win_fwd(win, win->base.y
                             - (win->base.scrolly + win->base.ht) + 1);
}

/*
 *   Add text to the scrollback buffer. 
 */
static void ossaddsb(osgen_txtwin_t *win, const char *p, size_t len, int draw)
{
    int startx;
    int starty;
    int line_len;

    /* if there's no scrollback buffer, ignore it */
    if (win->txtbuf == 0)
        return;

    /* note the starting x,y position, for redrawing purposes */
    startx = win->base.x - win->base.scrollx;
    starty = win->base.y - win->base.scrolly;

    /* we haven't added any characters to the line yet */
    line_len = 0;

    /*
     *   Copy the text into the screen buffer, respecting the circular
     *   nature of the screen buffer.  If the given text wraps lines,
     *   enter an explicit carriage return into the text to ensure that
     *   users can correctly count lines.
     */
    while (len != 0)
    {
        /* check what we have */
        switch(*p)
        {
        case OSGEN_ATTR:
            /* switch to the new attributes */
            win->base.txtattr = (unsigned char)*(p+1);

            /* 
             *   if we're at the start of the line, this is the start-of-line
             *   attribute - this will ensure that if we back up with '\r',
             *   we'll re-apply this attribute change 
             */
            if (win->base.x == 0)
                win->solattr = win->base.txtattr;

            /* add the two-byte sequence */
            osssb_add_bytes(win, p, 2);
            p += 2;
            len -= 2;
            break;

        case OSGEN_COLOR:
            /* switch to the new colors */
            win->base.txtfg = (unsigned char)*(p+1);
            win->base.txtbg = (unsigned char)*(p+2);

            /* 
             *   if we're at the start of the line, this is the new
             *   start-of-line color 
             */
            if (win->base.x == 0)
            {
                win->solfg = win->base.txtfg;
                win->solbg = win->base.txtbg;
            }

            /* add and skip the three-byte color sequence */
            osssb_add_bytes(win, p, 3);
            p += 3;
            len -= 3;
            break;
            
        case '\n':
            /* add the new line */
            osssb_new_line(win);

            /* draw the part of the line we added */
            if (draw)
                osgen_scrdisp(&win->base, startx, starty, line_len);

            /* skip this character */
            ++p;
            --len;

            /* reset to the next line */
            startx = win->base.x - win->base.scrollx;
            ++starty;
            line_len = 0;

            /* bring the cursor onto the screen if auto-vscrolling */
            if (draw)
                osgen_auto_vscroll(win);

            /* done */
            break;

        case '\r':
            /*
             *   We have a plain carriage return, which indicates that we
             *   should go back to the start of the current line and
             *   overwrite it.  (This is most likely to occur for the
             *   "[More]" prompt.)
             */

            /* set the free pointer back to the start of the current line */
            win->txtfree = osgen_get_line(win, win->base.y);

            /* switch back to the color as of the start of the line */
            win->base.txtfg = win->solfg;
            win->base.txtbg = win->solbg;
            win->base.txtattr = win->solattr;

            /* 
             *   since we're back at the start of the line, add a color code
             *   if necessary 
             */
            osssb_add_color_code(win);

            /* clear the window area of the line we're overwriting */
            if (draw && starty >= 0 && starty < (int)win->base.ht)
                ossclr(win->base.winy + starty, win->base.winx,
                       win->base.winy + starty,
                       win->base.winx + win->base.wid - 1,
                       win->base.oss_fillcolor);

            /* reset to the start of the line */
            line_len = 0;
            startx = 0;

            /* move to the left column */
            win->base.x = 0;

            /* skip this character */
            ++p;
            --len;

            /* done */
            break;

        default:
            /* for anything else, simply store the byte in the buffer */
            if (osssb_add_byte(win, *p))
            {
                /* note the maximum output position if appropriate */
                if (win->base.x > win->base.xmax)
                    win->base.xmax = win->base.x;

                /* adjust the output x position */
                win->base.x++;

                /* count the ordinary character in the display length */
                ++line_len;
            }
            
            /* skip the character */
            ++p;
            --len;
                
            /* done */
            break;
        }
    }

    /* 
     *   Add a safety null terminator after each addition, in case we need
     *   to look at the buffer before this line is finished.  
     */
    osssb_add_safety_null(win);

    /* display the added text */
    if (draw)
    {
        /* show the added text */
        osgen_scrdisp(&win->base, startx, starty, line_len);

        /* bring the cursor onto the screen if auto-vscrolling */
        osgen_auto_vscroll(win);
    }
}

/*
 *   Add an input line to the scrollback buffer.
 */
void ossaddsb_input(osgen_txtwin_t *win, char *p, int add_nl)
{
    size_t chars_rem;
    
    /* get the number of characters in the input to add */
    chars_rem = strlen(p);

    /* 
     *   add the input in chunks, wrapping whenever we reach the right edge
     *   of the window 
     */
    while (chars_rem != 0)
    {
        size_t wid_rem;
        size_t cur;
        
        /* if we're already past the edge, add a newline */
        if (win->base.x >= (int)win->base.wid)
            osssb_new_line(win);

        /* figure out how much space we have */
        wid_rem = win->base.wid - win->base.x;

        /* add as much as we can, up to the width remaining */
        cur = chars_rem;
        if (cur > wid_rem)
            cur = wid_rem;

        /* add the text */
        ossaddsb(win, p, cur, FALSE);

        /* skip this chunk */
        p += cur;
        chars_rem -= cur;
    }

    /* add a newline after the input if desired */
    if (add_nl)
        osssb_new_line(win);
    else
        osssb_add_safety_null(win);
}

/* ------------------------------------------------------------------------ */
/*
 *   End scrollback mode. 
 */
static void osgen_sb_mode_end()
{
    osgen_txtwin_t *win = S_sbmode_win;
    
    /* restore our original scroll position */
    if (win->base.scrolly > S_sbmode_orig_scrolly)
        osgen_scroll_win_back(win, win->base.scrolly - S_sbmode_orig_scrolly);
    else if (win->base.scrolly < S_sbmode_orig_scrolly)
        osgen_scroll_win_fwd(win, S_sbmode_orig_scrolly - win->base.scrolly);

    /* 
     *   adjust the input editing cursor position if the scrolling position
     *   doesn't match the original position 
     */
    S_gets_y += S_sbmode_orig_scrolly - win->base.scrolly;
    
    /* 
     *   restore our full window area, adding back in the top line that we
     *   removed to make room for the mode line 
     */
    win->base.winy -= 1;
    win->base.ht += 1;
    win->base.scrolly -= 1;
    
    /* redraw the top line, where we drew the mode line */
    ossclr(win->base.winy, win->base.winx,
           win->base.winy, win->base.winx + win->base.wid - 1,
           win->base.oss_fillcolor);
    osgen_scrdisp(&win->base, 0, 0, win->base.wid);

    /* 
     *   Delete the temporary copy of the input from the scrollback buffer.
     *   To do this, restore the x,y position and free pointer, then delete
     *   extra lines from the end of the buffer until the last line points
     *   to the same text it did before we added the command input text.  
     */
    win->base.x = S_sbmode_orig_x;
    win->txtfree = S_sbmode_orig_txtfree;
    while (win->line_count > 1
           && osgen_get_line_ptr(win, win->base.y) != S_sbmode_orig_last_line)
    {
        /* this is an added line - delete the line */
        win->line_count--;
        win->base.y--;
    }

    /* we're no longer in scrollback mode */
    S_sbmode_win = 0;
}

/*
 *   Draw the scrollback mode status line.  This is a line we draw at the top
 *   of the window to give a visual indication that we're in scrollback mode.
 *   (Actually, we draw this one line above the current top of the window,
 *   because we shrink the window vertically by one line while it's in
 *   scrollback mode specifically to make room for the mode line.)  
 */
static void osgen_draw_sb_mode_line(void)
{
    osgen_txtwin_t *win;
    int oss_color;

    /* get the current scrollback window */
    win = S_sbmode_win;

    /* there's nothing to do if there's no scrollback window */
    if (win == 0)
        return;

    /* get the color - use the reverse of the window's current color scheme */
    oss_color = ossgetcolor(win->base.fillcolor, win->base.txtfg, 0,
                            OSGEN_COLOR_TRANSPARENT);

    /* clear the area of the mode line */
    ossclr(win->base.winy - 1, win->base.winx,
           win->base.winy - 1, win->base.winx + win->base.wid - 1, oss_color);

    /* momentarily bring the mode line back into the window's area */
    win->base.winy -= 1;
    win->base.ht += 1;

    /* draw the mode line text */
    osgen_disp_trunc(&win->base, 0, 0, oss_color, OS_SBSTAT);

    /* undo our temporary adjustment of the window size */
    win->base.winy += 1;
    win->base.ht -= 1;
}

/*
 *   Process the event in scrollback mode.  If we're not already in
 *   scrollback mode, we'll enter scrollback mode; otherwise, we'll process
 *   the event in the already running scrollback.
 *   
 *   Returns true if we processed the event, false if we have terminated
 *   scrollback mode and want the caller to process the event as it normally
 *   would outside of scrollback mode.  
 */
static int osgen_sb_mode(osgen_txtwin_t *win, int event_type,
                         os_event_info_t *event_info)
{
    int ret;
    char c;
    int just_started;

    /* if we were doing scrollback in a different window, cancel it */
    if (S_sbmode_win != 0 && S_sbmode_win != win)
        osgen_sb_mode_end();

    /* note if we're just entering scrollback mode */
    just_started = (S_sbmode_win == 0);

    /* if scrollback mode isn't already in progress, set it up */
    if (just_started)
    {
        /* if there's no buffer, we can't enter scrollback mode */
        if (win->txtbuf == 0)
            return FALSE;

        /* 
         *   If the number of lines in the scrollback buffer doesn't exceed
         *   the number of lines displayed in the window, then don't bother
         *   entering scrollback mode.  If the window isn't at least two
         *   lines high, don't allow it either, as we need space to draw our
         *   mode line.  
         */
        if (win->line_count <= win->base.ht || win->base.ht < 2)
            return FALSE;

        /* remember the window involved in scrollback */
        S_sbmode_win = win;
        
        /* 
         *   Shrink the window one line at the top, to make room for the mode
         *   line.  Compensate by bumping the scrolling position forward a
         *   line, to keep the lines that will remain visible in the rest of
         *   the window at the same place on the screen.  
         */
        win->base.winy += 1;
        win->base.ht -= 1;
        win->base.scrolly += 1;

        /*
         *   Temporarily add the command buffer to the scrollback buffer, so
         *   that it shows up when we're scrolling.  To facilitate removing
         *   the added text later, remember the current free pointer, last
         *   line pointer, x document position.  
         */
        S_sbmode_orig_x = win->base.x;
        S_sbmode_orig_txtfree = win->txtfree;
        S_sbmode_orig_last_line = osgen_get_line_ptr(win, win->base.y);
        ossaddsb_input(win, S_gets_buf, FALSE);

        /* remember the original scrolling position */
        S_sbmode_orig_scrolly = win->base.scrolly;

        /* draw the mode line */
        osgen_draw_sb_mode_line();
    }

    /* presume we'll handle the event */
    ret = TRUE;

    /* check the event */
    switch(event_type)
    {
    case OS_EVT_KEY:
        /* 
         *   If it's a regular key, exit scrollback mode immediately, and
         *   let the caller handle the event normally.  This allows the user
         *   to resume typing even while scrollback is active; we'll simply
         *   cancel the scrollback and resume editing with the event that
         *   canceled the scrollback.  
         */
        c = event_info->key[0];
        if ((unsigned char)c >= 32 || c == '\n' || c == '\r' || c == 8)
        {
            /* it's a regular key - terminate scrollback mode */
            osgen_sb_mode_end();

            /* let the caller handle the event */
            ret = FALSE;
        }

        /* if it's the escape key, we're done */
        if (c == 27)
        {
            osgen_sb_mode_end();
            break;
        }

        /* if it's not a special keystroke, we're done with it */
        if (c != '\0')
            break;

        /* it's a special keystroke - check the command ID */
        switch(event_info->key[1])
        {
        case CMD_SCR:
            /* if we're not just toggling in to scrollback mode, toggle out */
            if (!just_started)
                osgen_sb_mode_end();
            break;
            
        case CMD_KILL:
        case CMD_EOF:
            /* it's a termination key - cancel scrollback mode */
            osgen_sb_mode_end();
            break;

        case CMD_UP:
            /* scroll back a line */
            osgen_scroll_win_back(win, 1);
            break;

        case CMD_DOWN:
            /* scroll forward a line */
            osgen_scroll_win_fwd(win, 1);
            break;
            
        case CMD_PGUP:
            /* move back by one window height */
            osgen_scroll_win_back(win, win->base.ht > 1
                                  ? win->base.ht - 1 : 1);
            break;
            
        case CMD_PGDN:
            /* move forward by one window height */
            osgen_scroll_win_fwd(win, win->base.ht > 1
                                 ? win->base.ht - 1 : 1);
            break;
        }

        /* done with the key */
        break;

    default:
        /* 
         *   ignore any other events we receive, but consider them processed
         *   - even though we're ignoring the event, we're deliberately
         *   ignoring the event as our way of processing it 
         */
        break;
    }

    /* return the 'processed' indication to the caller */
    return ret;
}

/*
 *   Display a line of text in the given ordinary text window 
 */
static void osgen_scrdisp_txt(osgen_txtwin_t *win, int x, int y, int len)
{
    int fg;
    int bg;
    int attr;
    int oss_color;
    char buf[64];
    char *bufp;
    char *p;
    int scanx;

    /* start out in ordinary text mode */
    fg = OSGEN_COLOR_TEXT;
    bg = OSGEN_COLOR_TRANSPARENT;
    attr = 0;
    oss_color = ossgetcolor(fg, bg, attr, win->base.fillcolor);

    /* get a pointer to the start of the desired line */
    p = osgen_get_line(win, y + win->base.scrolly);
    if (p == 0)
        return;

    /* get the window-relative x coordinate of the start of the line */
    scanx = -win->base.scrollx;

    /* 
     *   Scan the line.  We must scan from the start of the line, even if we
     *   don't want to display from the start of the line, to make sure we
     *   take into account any color and attribute settings stored in the
     *   line.  
     */
    for (bufp = buf ; *p != '\0' && len != 0 ; p = ossadvsp(win, p))
    {
        /* check for special escape codes */
        switch(*p)
        {
        case OSGEN_ATTR:
            /* skip the escape byte */
            p = ossadvsp(win, p);

            /* get the new attribute code */
            attr = *p;

            /* set the new colors */
            goto change_color;

        case OSGEN_COLOR:
            /* skip the escape byte */
            p = ossadvsp(win, p);

            /* get the foreground color and skip it */
            fg = *p;
            p = ossadvsp(win, p);

            /* get the background color */
            bg = *p;
            goto change_color;

        change_color:
            /* flush the buffer */
            if (bufp != buf)
            {
                /* display the contents of the buffer */
                *bufp = '\0';
                osgen_disp_trunc(&win->base, y, x, oss_color, buf);

                /* adjust the column position for the display */
                x += bufp - buf;

                /* reset the buffer */
                bufp = buf;
            }

            /* translate the new text color */
            oss_color = ossgetcolor(fg, bg, attr, win->base.fillcolor);
            break;

        default:
            /* if the buffer is full, flush it */
            if (bufp == buf + sizeof(buf) - 1)
            {
                /* display the buffer */
                *bufp = '\0';
                osgen_disp_trunc(&win->base, y, x, oss_color, buf);

                /* adjust the column position for the display */
                x += bufp - buf;

                /* empty the buffer */
                bufp = buf;
            }

            /* 
             *   if we've reached the starting x coordinate, add this
             *   character to the buffer; if we haven't, we can ignore the
             *   character, since in that case we're just scanning for
             *   escape codes in the line before the part we want to display 
             */
            if (scanx >= x)
            {
                /* add it to the buffer */
                *bufp++ = *p;

                /* count it against the length remaining to be displayed */
                --len;
            }

            /* adjust the x coordinate of our scan */
            ++scanx;

            /* done */
            break;
        }
    }

    /* display the last portion of the line if anything's left */
    if (bufp != buf)
    {
        /* null-terminate at the current point */
        *bufp = '\0';

        /* display it */
        osgen_disp_trunc(&win->base, y, x, oss_color, buf);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Special routines for Text Grid windows
 */

/*
 *   Clear the text and color arrays for a section of a grid window.  Sets
 *   each cleared character's text to a space, and sets the color to
 *   text/transparent.  
 */
static void osgen_gridwin_clear_ptr(char *txtp, osgen_charcolor_t *colorp,
                                    size_t len)
{
    /* loop through the grid from the starting offset */
    for ( ; len != 0 ; --len, ++txtp, ++colorp)
    {
        /* set this character's text to a space */
        *txtp = ' ';

        /* set this character's color to text/transparent */
        colorp->fg = OSGEN_COLOR_TEXT;
        colorp->bg = OSGEN_COLOR_TRANSPARENT;
    }
}

/*
 *   Clear the text and color arrays for a section of a grid window.  Starts
 *   at the given character offset in the grid arrays, and clears for the
 *   given number of character positions.  Sets each cleared character's
 *   text to a space, and sets the color to text/transparent.  
 */
static void osgen_gridwin_clear(osgen_gridwin_t *win, size_t ofs, size_t len)
{
    /* clear our array in the selected areas */
    osgen_gridwin_clear_ptr(win->grid_txt + ofs, win->grid_color + ofs, len);
}


/*
 *   Resize a text grid window.  This must be called whenever the on-screen
 *   size of the window is changed, or we need to write outside the current
 *   bounds of the array, so that we can expand our internal size allocation
 *   if the window is now bigger than our internal text/color arrays.  
 */
static void osgen_gridwin_resize(osgen_gridwin_t *win,
                                 size_t new_wid, size_t new_ht)
{
    /* if the window's size is now bigger than the grid, expand the grid */
    if (new_wid > win->grid_wid || new_ht > win->grid_ht)
    {
        char *new_txt;
        size_t ofs;
        osgen_charcolor_t *new_color;
        char *tsrc, *tdst;
        osgen_charcolor_t *csrc, *cdst;
        size_t y;

        /*  
         *   use the larger of the old size and the new size, so that the
         *   window only expands (this somewhat simplifies copying the old
         *   to the new) 
         */
        if (win->grid_wid > new_wid)
            new_wid = win->grid_wid;
        if (win->grid_ht > new_ht)
            new_ht = win->grid_ht;

        /* allocate the new arrays */
        new_txt = (char *)osmalloc(new_wid * new_ht);
        new_color = (osgen_charcolor_t *)osmalloc(
            new_wid * new_ht * sizeof(new_color[0]));

        /* copy the old grid to the new grid */
        tsrc = win->grid_txt;
        csrc = win->grid_color;
        tdst = new_txt;
        cdst = new_color;
        for (y = 0, ofs = 0 ; y < win->grid_ht ; ++y)
        {
            /* copy the old text and color data */
            memcpy(tdst, tsrc, win->grid_wid);
            memcpy(cdst, csrc, win->grid_wid * sizeof(cdst[0]));

            /* clear the rest of the line if expanding the width */
            osgen_gridwin_clear_ptr(tdst + win->grid_wid,
                                    cdst + win->grid_wid,
                                    new_wid - win->grid_wid);

            /* advance the pointers */
            tsrc += win->grid_wid;
            csrc += win->grid_wid;
            tdst += new_wid;
            cdst += new_wid;
        }

        /* clear all remaining lines */
        for ( ; y < new_ht ; ++y, tdst += new_wid, cdst += new_wid)
            osgen_gridwin_clear_ptr(tdst, cdst, new_wid);

        /* delete the old buffers */
        osfree(win->grid_txt);
        osfree(win->grid_color);

        /* set the new buffers and sizes */
        win->grid_txt = new_txt;
        win->grid_color = new_color;
        win->grid_wid = new_wid;
        win->grid_ht = new_ht;
    }
}

/*
 *   Display a line of text in the given grid window 
 */
static void osgen_scrdisp_grid(osgen_gridwin_t *win, int x, int y, int len)
{
    char *txtp;
    osgen_charcolor_t *colorp;
    size_t ofs;
    char buf[64];
    char *dst;
    char fg, bg;
    int oss_color;

    /* 
     *   calculate the offset into our arrays: get the document coordinates
     *   (by adjusting for the scrolling offset), multiply the document row
     *   number by the row width, and add the document column number 
     */
    ofs = ((y - win->base.scrolly) * win->grid_wid) + (x - win->base.scrollx);

    /* start at the calculated offset */
    txtp = win->grid_txt + ofs;
    colorp = win->grid_color + ofs;

    /* start with the first character's color */
    fg = colorp->fg;
    bg = colorp->bg;

    /* calculate the starting oss-level color */
    oss_color = ossgetcolor(fg, bg, 0, win->base.fillcolor);

    /* 
     *   scan the text, flushing when we fill up the buffer or encounter a
     *   new text color 
     */
    for (dst = buf ; ; --len, ++txtp, ++colorp)
    {
        /* 
         *   if we have a color change, or we've exhausted the requested
         *   length, or the buffer is full, display what we have in the
         *   buffer so far 
         */
        if (len == 0
            || dst == buf + sizeof(buf) - 1
            || colorp->fg != fg || colorp->bg != bg)
        {
            /* null-terminate the buffer and display it */
            *dst = '\0';
            osgen_disp_trunc(&win->base, y, x, oss_color, buf);

            /* count the output column change */
            x += dst - buf;

            /* reset the buffer write pointer */
            dst = buf;

            /* if we have a color change, calculate the new color */
            if (colorp->fg != fg || colorp->bg != bg)
            {
                /* remember the new color */
                fg = colorp->fg;
                bg = colorp->bg;

                /* calculate the new oss-level color */
                oss_color = ossgetcolor(fg, bg, 0, win->base.fillcolor);
            }
        }

        /* if we've exhausted the request, we're done */
        if (len == 0)
            break;

        /* add this character from the text grid to our output buffer */
        *dst++ = *txtp;
    }
}

/*
 *   Write text into a grid window
 */
static void osgen_gridwin_write(osgen_gridwin_t *win,
                                const char *txt, size_t len)
{
    const char *p;
    size_t rem;
    int xmax, ymax;
    int x, y;
    int startx;
    int winx;
    size_t ofs;
    char *tdst;
    osgen_charcolor_t *cdst;

    /*
     *   First, scan the text to check for writing beyond the end of the
     *   current text array.  If we write anything beyond the bounds of the
     *   current array, we'll need to expand the array accordingly. 
     */
    xmax = x = win->base.x;
    ymax = y = win->base.y;
    for (p = txt, rem = len ; rem != 0 ; ++p, --rem)
    {
        /* check what we have */
        switch(*p)
        {
        case '\n':
            /* move to the start of the next line */
            x = 0;
            ++y;
            break;

        case '\r':
            /* move to the start of the current line */
            x = 0;
            break;

        default:
            /* 
             *   everything else takes up a character cell; if this is the
             *   highest x/y position so far, note it 
             */
            if (y > ymax)
                ymax = y;
            if (x > xmax)
                xmax = x;

            /* move the cursor right */
            ++x;
            break;
        }
    }

    /* expand our text array if we're writing outside the current bounds */
    if (xmax >= (int)win->grid_wid || ymax >= (int)win->grid_ht)
        osgen_gridwin_resize(win, xmax + 1, ymax + 1);

    /* start at the current window coordinates */
    x = win->base.x;
    y = win->base.y;

    /* calculate the starting offset in the array for writing */
    ofs = y * win->grid_wid + x;
    tdst = win->grid_txt + ofs;
    cdst = win->grid_color + ofs;

    /* note the new maximum write positions */
    if (xmax > win->base.xmax)
        win->base.xmax = xmax;
    if (ymax > win->base.ymax)
        win->base.ymax = ymax;

    /* start at the current column */
    startx = x;
    winx = x - win->base.scrollx;

    /* now scan the text again, writing it to the grid and displaying it */
    for (p = txt, rem = len ; ; ++p, --rem)
    {
        /* 
         *   if we're at a newline or the end of the text to display,
         *   display the section of the current line that we just built 
         */
        if (rem == 0 || *p == '\n' || *p == '\r')
        {
            /* if we're not in deferred redraw mode, draw this section */
            if (!S_deferred_redraw
                && !(win->base.flags & OSGEN_DEFER_REDRAW)
                && x != startx)
                osgen_scrdisp(&win->base, winx, y, x - startx);

            /* if we're out of text to display, we're done */
            if (rem == 0)
                break;

            /* apply any newline */
            if (*p == '\n')
            {
                /* move to the next line */
                x = 0;
                ++y;
            }
            else if (*p == '\r')
            {
                /* move to the start of the current line */
                x = 0;
            }

            /* note the starting coordinates of the next chunk */
            startx = x;
            winx = x - win->base.scrollx;

            /* calculate the new array output offset */
            ofs = y * win->grid_wid + x;
            tdst = win->grid_txt + ofs;
            cdst = win->grid_color + ofs;
        }
        else
        {
            /* add this character to our array */
            *tdst = *p;
            cdst->fg = win->base.txtfg;
            cdst->bg = win->base.txtbg;

            /* ajust the output pointers */
            ++tdst;
            ++cdst;

            /* adjust the column counter */
            ++x;
        }
    }

    /* set the new output position in the window */
    win->base.x = x;
    win->base.y = y;
}


/* ------------------------------------------------------------------------ */
/*
 *   Generic banner window routines 
 */

/*
 *   Display a window's text from the given starting position for the given
 *   number of characters (counting only displayed characters; escape
 *   sequences don't count).  The starting position is given in
 *   window-relative coordinates.  
 */
static void osgen_scrdisp(osgen_win_t *win, int x, int y, int len)
{
    /* 
     *   if the text is entirely outside the window's display area, there's
     *   nothing to display, so ignore the request 
     */
    if (y < 0 || (size_t)y >= win->ht
        || x + len <= 0 || (size_t)x >= win->wid)
        return;

    /* draw according to window type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        osgen_scrdisp_txt((osgen_txtwin_t *)win, x, y, len);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        osgen_scrdisp_grid((osgen_gridwin_t *)win, x, y, len);
        break;
    }
}


/* 
 *   redraw a window 
 */
static void osgen_redraw_win(osgen_win_t *win)
{
    size_t y;

    /* clear the window's area on the screen */
    ossclr(win->winy, win->winx,
           win->winy + win->ht - 1, win->winx + win->wid - 1,
           win->oss_fillcolor);

    /* display each line in the window */
    for (y = 0 ; y < win->ht ; ++y)
        osgen_scrdisp(win, 0, y, win->wid);
}

/* redraw the entire screen */
void os_redraw(void)
{
    /* 
     *   force a redraw of the entire screen by setting the global
     *   pending-redraw flag 
     */
    S_deferred_redraw = TRUE;

    /* go redraw it */
    osssb_redraw_if_needed();
    
}

/* redraw a window, if it needs redrawing */
static void osgen_redraw_win_if_needed(int global_deferred, osgen_win_t *win)
{
    osgen_win_t *chi;

    /* 
     *   if this window needs redrawing, or we have a global deferred redraw,
     *   redraw the window 
     */
    if (global_deferred || (win->flags & OSGEN_DEFER_REDRAW) != 0)
    {
        /* clear the window-specific deferred-redraw flag */
        win->flags &= ~OSGEN_DEFER_REDRAW;

        /* redraw the window */
        osgen_redraw_win(win);
    }

    /* redraw this window's children if necessary */
    for (chi = win->first_child ; chi != 0 ; chi = chi->nxt)
        osgen_redraw_win_if_needed(global_deferred, chi);
}

/* redraw the screen if necessary */
void osssb_redraw_if_needed()
{
    int global_deferred = S_deferred_redraw;

    /* we're explicitly redrawing, so cancel any pending deferred redraw */
    S_deferred_redraw = FALSE;

    /* redraw the root window, which will redraw its children */
    if (S_main_win != 0)
        osgen_redraw_win_if_needed(global_deferred, &S_main_win->base);

    /* if the redraw is global, redraw other global features */
    if (global_deferred)
    {
        /* redraw the scrollback mode line, if appropriate */
        osgen_draw_sb_mode_line();

        /* redraw any command line under construction */
        osgen_gets_redraw_cmdline();
    }
}

/* move the cursor to the default position */
void osssb_cursor_to_default_pos(void)
{
    osgen_win_t *win;

    /* if we're in plain mode, ignore it */
    if (os_f_plain)
        return;

    /* if we're using a special cursor position, do nothing */
    if (S_sbmode_win != 0)
    {
        /* locate at the scrollback mode line */
        ossloc(S_sbmode_win->base.winy - 1, S_sbmode_win->base.winx);
    }
    else if (S_special_cursor_pos)
    {
        /* locate at the special cursor position */
        ossloc(S_special_cursor_y, S_special_cursor_x);
    }
    else
    {
        /* 
         *   if we have a default window, put the cursor at the last text
         *   position in the default window 
         */
        if ((win = &S_default_win->base) != 0)
            ossloc(win->winy + win->y - win->scrolly,
                   win->winx + win->x - win->scrollx);
    }
}

/*
 *   Lay out the given window 
 */
static void oss_lay_out_window(osgen_win_t *win)
{
    osgen_win_t *chi;

    /* 
     *   if we have a parent, take space from our parent window; otherwise,
     *   assume that the caller has already laid out our main area, in which
     *   case we just have to adjust for our children 
     */
    if (win->parent != 0)
    {
        int siz;
        int base_size;
        int x, y, wid, ht;

        /* 
         *   our size and area will be taken from our parent's, so get the
         *   parent window's current area 
         */
        x = win->parent->winx;
        y = win->parent->winy;
        wid = win->parent->wid;
        ht = win->parent->ht;

        /* get the window's size in character cells */
        switch(win->size_type)
        {
        case OS_BANNER_SIZE_ABS:
            /* the size is given in character cells */
            siz = win->size;
            break;

        case OS_BANNER_SIZE_PCT:
            /* 
             *   the size is given as a percentage of the parent's size - get
             *   the appropriate dimension from the parent's size 
             */
            base_size = (win->alignment == OS_BANNER_ALIGN_LEFT
                         || win->alignment == OS_BANNER_ALIGN_RIGHT
                         ? wid : ht);

            /* calculate the percentage of the full size */
            siz = (win->size * base_size) / 100;
            break;
        }

        /* allocate space to the window according to its alignment */
        switch(win->alignment)
        {
        case OS_BANNER_ALIGN_TOP:
            /* 
             *   assign the window the full width of the parent, and give it
             *   the requested height, up to the available height 
             */
            win->wid = wid;
            win->winx = x;
            win->ht = (siz <= ht ? siz : ht);
            win->winy = y;

            /* take the window's space away from the top of the parent */
            ht -= win->ht;
            y += win->ht;
            break;

        case OS_BANNER_ALIGN_BOTTOM:
            /* give the window space at the bottom of the parent window */
            win->wid = wid;
            win->winx = x;
            win->ht = (siz <= ht ? siz : ht);
            win->winy = y + ht - win->ht;

            /* deduct the window from the parent area */
            ht -= win->ht;
            break;

        case OS_BANNER_ALIGN_LEFT:
            /* give the window space at the left of the parent area */
            win->wid = (siz <= wid ? siz : wid);
            win->winx = x;
            win->ht = ht;
            win->winy = y;

            /* deduct the window from the parent area */
            wid -= win->wid;
            x += win->wid;
            break;

        case OS_BANNER_ALIGN_RIGHT:
            /* give the window space at the right of the remaining area */
            win->wid = (siz <= wid ? siz : wid);
            win->winx = x + wid - win->wid;
            win->ht = ht;
            win->winy = y;

            /* deduct the window from the parent area */
            wid -= win->wid;
            break;
        }

        /* adjust our parent's area for the removal of our area */
        win->parent->winx = x;
        win->parent->winy = y;
        win->parent->wid = wid;
        win->parent->ht = ht;
    }

    /* lay out our children */
    for (chi = win->first_child ; chi != 0 ; chi = chi->nxt)
        oss_lay_out_window(chi);

    /* make any necessary adjustments */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /*
         *   If this is the current active scrollback-mode window, we must
         *   make the same adjustment that we make on entering scrollback
         *   mode: shrink the window one line from the top, to make room for
         *   the scrollback-mode status line.  
         */
        if (win == &S_sbmode_win->base)
        {
            /* it's the scrollback window - take out the mode line */
            win->winy += 1;
            win->ht -= 1;
        }
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* make sure the window's text/color buffers are large enough */
        osgen_gridwin_resize((osgen_gridwin_t *)win, win->wid, win->ht);
        break;
    }
}

/*
 *   Recalculate the window layout and redraw the screen 
 */
static void osgen_recalc_layout()
{
    /* start at the root window */
    if (S_main_win != 0)
    {
        int ht;

        /* start by giving the entire screen to the main window */
        S_main_win->base.winx = 0;
        S_main_win->base.winy = 0;
        S_main_win->base.wid = G_oss_screen_width;
        S_main_win->base.ht = G_oss_screen_height;

        /* lay out the main window and its children */
        oss_lay_out_window(&S_main_win->base);

        /* recalculate the main window's page size */
        G_os_linewidth = S_main_win->base.wid;
        ht = S_main_win->base.ht;
        G_os_pagelength = (ht > 2 ? ht - 2 : ht > 1 ? ht - 1 : ht);
    }

    /* schedule a redraw of the entire screen */
    S_deferred_redraw = TRUE;
}

/*
 *   Receive notification that the screen was resized.  We'll recalculate
 *   the banner window layout.  
 */
void osssb_on_resize_screen()
{
    /* recalculate the window layout */
    osgen_recalc_layout();

    /* immediately update the screen */
    osssb_redraw_if_needed();

    /* set the cursor back to the default position */
    osssb_cursor_to_default_pos();
}

# else /* USE_SCROLLBACK */
/* 
 *   for the non-scrollback version, add-to-scrollback is just a dummy
 *   function: if we're not saving scrollback, we obviously have no need to
 *   save any information added to the buffer 
 */
static void ossaddsb(struct osgen_win_t *win, char *p, size_t len, int draw)
{
    /* do nothing */
}

/* 
 *   for the non-scrollback version, there's nothing we need to do on
 *   resizing the screen, as we don't do anything fancy with the layout 
 */
void osssb_on_resize_screen()
{
    /* do nothing */
}

# endif /* USE_SCROLLBACK */

/* ------------------------------------------------------------------------ */

#ifdef USE_STATLINE

/* current statusline score-area string */
static osfar_t char S_scorebuf[135];

/* statusline left-string reset point */
static osfar_t char *S_stat_reset_free;
static osfar_t int S_stat_reset_x;

/* update the right portion of the statusline */
static void osgen_update_stat_right()
{
    osgen_txtwin_t *win;
    int wid_rem;
    size_t char_len;
    
    /* 
     *   if there's no statusline window, or no reset point, we can't do
     *   anything right now 
     */
    if ((win = S_status_win) == 0 || S_stat_reset_free == 0)
        return;

    /* 
     *   set the statusline position back to where it was when we finished
     *   with the statusline itself
     */
    win->txtfree = S_stat_reset_free;
    win->base.x = S_stat_reset_x;

    /* if there's no space, don't draw anything */
    if (win->base.x > (int)win->base.wid)
        return;

    /* figure out how much space in the window we have */
    wid_rem = win->base.wid - win->base.x;

    /* figure out how much we're adding */
    char_len = strlen(S_scorebuf);

    /* 
     *   if we're adding more than we have room for, add a space and then
     *   add the buffer contents 
     */
    if ((int)char_len + 1 >= wid_rem)
    {
        /* add a space */
        ossaddsb(win, " ", 1, TRUE);

        /* add the right-half string */
        ossaddsb(win, S_scorebuf, strlen(S_scorebuf), TRUE);
    }
    else
    {
        /* add spaces to right-align the scorebuf text */
        while (wid_rem > (int)char_len + 1)
        {
            char buf[64];
            size_t cur;
            
            /* add the remaining spaces, up to a buffer-full */
            cur = wid_rem - char_len - 1;
            if (cur > sizeof(buf) - 1)
                cur = sizeof(buf) - 1;

            /* make a buffer-full of spaces */
            memset(buf, ' ', cur);

            /* display it */
            ossaddsb(win, buf, cur, TRUE);

            /* deduct the amount we displayed from the width remaining */
            wid_rem -= cur;
        }

        /* add the right-half string */
        ossaddsb(win, S_scorebuf, strlen(S_scorebuf), TRUE);
    }
}

/*
 *   Set the status mode.  In status mode 0, text displayed via os_print and
 *   the like will display to the main window; in mode 1, text displayed will
 *   go to the default status line window, until we reach a newline, at which
 *   point we'll switch to status mode 2 and text will go nowhere.  
 */
void os_status(int stat)
{
    /* if we're in 'plain' mode, suppress all statusline output */
    if (os_f_plain)
    {
        /* check the requested mode */
        switch(stat)
        {
        case 0:
            /* switching to main text mode */
            status_mode = 0;
            break;

        case 1:
            /* switching to statusline mode - suppress all output */
            status_mode = 2;
            break;

        case 2:
            /* suppress mode */
            status_mode = 2;
            break;
        }

        /* done */
        return;
    }

    /* if there's no statusline window, create one */
    if (S_status_win == 0)
    {
        /* create the statusline window as a child of the main window */
        S_status_win = osgen_create_txtwin(OS_BANNER_FIRST, 0, S_main_win,
                                           512, 1);

        /* if that succeeded, set up the window */
        if (S_status_win != 0)
        {
            /* set up the statusline window at the top of the screen */
            S_status_win->base.alignment = OS_BANNER_ALIGN_TOP;
            S_status_win->base.size = 1;
            S_status_win->base.size_type = OS_BANNER_SIZE_ABS;

            /* use the default statusline color in this window */
            S_status_win->base.txtfg = OSGEN_COLOR_STATUSLINE;
            S_status_win->base.txtbg = OSGEN_COLOR_TRANSPARENT;
            S_status_win->base.fillcolor = OSGEN_COLOR_STATUSBG;

            /* cache the oss translations of the colors */
            S_status_win->base.oss_fillcolor =
                ossgetcolor(OSGEN_COLOR_STATUSLINE, OSGEN_COLOR_STATUSBG,
                            0, 0);

            /* recalculate the window layout */
            osgen_recalc_layout();
        }
    }

    /* if entering status mode 1, clear the statusline */
    if (stat == 1 && status_mode != 1 && S_status_win != 0)
    {
        /* switch the default window to the status line */
        S_default_win = S_status_win;

        /* clear the statusline window */
        osgen_clear_win(&S_status_win->base);

        /* forget the score reset point */
        S_stat_reset_free = 0;
    }

    /* if we're leaving status mode 1, finish the statusline */
    if (status_mode == 1 && stat != 1 && S_status_win != 0)
    {
        /* 
         *   remember the current statusline window settings as the "reset"
         *   point - this is where we reset the buffer contents whenever we
         *   want to add the right-half string 
         */
        S_stat_reset_free = S_status_win->txtfree;
        S_stat_reset_x = S_status_win->base.x;

        /* update the right-half string */
        osgen_update_stat_right();
    }

    /* switch to the new mode */
    status_mode = stat;

    /* check the mode */
    if (status_mode == 0)
    {
        /* switching to the main window */
        S_default_win = S_main_win;
    }
    else if (status_mode == 2)
    {
        /* 
         *   entering post-status mode - ignore everything, so set the
         *   default window to null 
         */
        S_default_win = 0;
    }
}

int os_get_status()
{
    return status_mode;
}

/* Set score to a string value provided by the caller */
void os_strsc(const char *p)
{
    size_t copy_len;

    /* copy the score, if a value was given */
    if (p != 0)
    {
        /* limit the copying length to our buffer size */
        copy_len = strlen(p);
        if (copy_len > sizeof(S_scorebuf) - 1)
            copy_len = sizeof(S_scorebuf) - 1;
        
        /* copy the text and null-terminate it */
        memcpy(S_scorebuf, p, copy_len);
        S_scorebuf[copy_len] = '\0';
    }

    /* update the statusline window */
    osgen_update_stat_right();
}

/*
 *   Set the score.  If cur == -1, the LAST score set with a non-(-1) cur is
 *   displayed; this is used to refresh the status line without providing a
 *   new score (for example, after exiting scrollback mode).  Otherwise, the
 *   given current score (cur) and turncount are displayed, and saved in
 *   case cur==-1 on the next call.  
 */
void os_score(int cur, int turncount)
{
    char buf[20];

    /* check for the special -1 turn count */
    if (turncount == -1)
    {
        /* it's turn "-1" - we're simply redrawing the score */
        os_strsc((char *)0);
    }
    else
    {
        /* format the score */
        sprintf(buf, "%d/%d", cur, turncount);

        /* display the score string */
        os_strsc(buf);
    }
}


/* ------------------------------------------------------------------------ */
/*
 *   Set the terminal into 'plain' mode: disables status line,
 *   scrollback, command editing.
 */
void os_plain(void)
{
    /* set the 'plain' mode flag */
    os_f_plain = 1;
}

/*
 *   display text to the default window 
 */
void os_printz(const char *str)
{
    /* write using the base counted-length routine */
    os_print(str, strlen(str));
}

/*
 *   display text to the default window
 */
void os_print(const char *str, size_t len)
{
    osgen_txtwin_t *win;
    const char *p;
    const char *startp;
    size_t rem;

    /* determine what to do based on the status mode */
    switch(status_mode)
    {
    case 2:
        /* we're in the post-status-line mode - suppress all output */
        break; 
        
    case 0:
        /* we're showing the text in the default window */
        if (os_f_plain)
        {
            /* plain mode - simply write it to stdout */
            printf("%.*s", (int)len, str);
        }
        else
        {
            /* normal mode - write to the default window, if there is one */
            win = S_default_win;
            if (win != 0)
            {
                /* write the text to the window buffer */
                ossaddsb(win, str, len, TRUE);

                /* 
                 *   move the cursor to the new location, if we're not hiding
                 *   updates for the moment 
                 */
                if (!S_deferred_redraw
                    && !(win->base.flags & OSGEN_DEFER_REDRAW))
                    ossloc(win->base.winy + win->base.y - win->base.scrolly,
                           win->base.winx + win->base.x - win->base.scrollx);
            }
        }

        /* done */
        break;
        
    case 1:
        /* 
         *   Status line contents.  Ignore the status line in 'plain' mode
         *   or if there's no statusline window.  
         */
        if (os_f_plain || (win = S_status_win) == 0)
            break;

        /* 
         *   Skip leading newlines at the start of the statusline output.
         *   Only do this if we don't already have anything buffered, since
         *   a newline after some other text indicates the end of the status
         *   line and thus can't be ignored.  
         */
        p = str;
        rem = len;
        if (win->base.winy == 0 || win->base.winx == 0)
        {
            /* the buffer is empty, so skip leading newlines */
            for ( ; rem != 0 && *p == '\n' ; ++p, --rem) ;

            /* if that leaves nothing, we're done */
            if (rem == 0)
                break;

            /* 
             *   add a space before the first character, so that we always
             *   have a space at the left edge of the status line 
             */
            ossaddsb(win, " ", 1, TRUE);
        }

        /* scan for a newline; if we find one, it's the end of the status */
        for (startp = p ; rem != 0 && *p != '\n' ; ++p, --rem)
        {
            /* skip escapes */
            switch (*p)
            {
            case OSGEN_ATTR:
                /* skip the extra byte */
                p += 1;
                rem -= 1;
                break;
                
            case OSGEN_COLOR:
                /* skip the extra two bytes */
                p += 2;
                rem -= 2;
                break;

            default:
                /* everything else is one byte long */
                break;
            }
        }

        /* add this text to the statusline window */
        ossaddsb(win, startp, p - startp, TRUE);

        /* finish up if we found a newline */
        if (*p == '\n')
        {
            /* switch to status mode 2 */
            os_status(2);
        }
        
        /* done */
        break;
    }
}

void os_flush(void)
{
    /* 
     *   we don't buffer output ourselves, so there's normally nothing to do
     *   here; but if we're in 'plain' mode, let stdio know about the flush,
     *   since it might be buffering output on our behalf 
     */
    if (os_f_plain)
        fflush(stdout);
}

void os_update_display(void)
{
    /* there's nothing we need to do */
}

# ifdef USE_HISTORY
/*
 *   For command line history, we must have some buffer space to store
 *   past command lines.  We will use a circular buffer:  when we move
 *   the pointer past the end of the buffer, it wraps back to the start
 *   of the buffer.  A "tail" indicates the oldest line in the buffer;
 *   when we need more room for new text, we advance the tail and thereby
 *   lose the oldest text in the buffer.
 */
static osfar_t char *histbuf = 0;
static osfar_t char *histhead = 0;
static osfar_t char *histtail = 0;

/*
 *   ossadvhp advances a history pointer, and returns the new pointer.
 *   This function takes the circular nature of the buffer into account
 *   by wrapping back to the start of the buffer when it hits the end.
 */
char *ossadvhp(char *p)
{
    if (++p >= histbuf + HISTBUFSIZE)
        p = histbuf;
    return p;
}

/*
 *   ossdechp decrements a history pointer, wrapping the pointer back
 *   to the top of the buffer when it reaches the bottom.
 */
char *ossdechp(char *p)
{
    if (p == histbuf)
        p = histbuf + HISTBUFSIZE;
    return p - 1;
}

/*
 *  osshstcpy copies from a history buffer into a contiguous destination
 *  buffer, wrapping the history pointer if need be.  One null-terminated
 *  string is copied.
 */
void osshstcpy(char *dst, char *hst)
{
    while (*hst != '\0')
    {
        *dst++ = *hst;
        hst = ossadvhp(hst);
    }
    *dst = '\0';
}

/*
 *   ossprvcmd returns a pointer to the previous history command, given
 *   a pointer to a history command.  It returns a null pointer if the
 *   given history command is the first in the buffer.
 */
char *ossprvcmd(char *hst)
{
    /* check to see if we're already at the fist command */
    if (hst == histtail)
        return 0;

    /* back up to the previous null byte */
    hst = ossdechp(hst);

    /* scan back to the previous line */
    do
    {
        hst = ossdechp(hst);
    } while (*hst && hst != histtail);

    /* step over the null byte to the start of the following line */
    if (*hst == 0)
        hst = ossadvhp(hst);

    /* return the result */
    return hst;
}

/*
 *   ossnxtcmd returns a pointer to the next history command, given
 *   a pointer to a history command.  It returns a null pointer if the
 *   given command is already past the last command.
 */
char *ossnxtcmd(char *hst)
{
    /* check to see if we're already past the last line */
    if (hst == histhead)
        return 0;

    /* scan forward to the next null byte */
    while(*hst != '\0')
        hst = ossadvhp(hst);

    /* scan past the null onto the new command */
    hst = ossadvhp(hst);

    /* return the pointer */
    return hst;
}
# endif /* USE_HISTORY */

/* ------------------------------------------------------------------------ */
/*
 *   Display an input line under construction from the given character
 *   position.  If 'delta_yscroll' is non-null, then we'll scroll the window
 *   vertically if necessary and fill in '*delta_yscroll' with the number of
 *   lines we scrolled by.  
 */
static void ossdsp_str(osgen_win_t *win, int y, int x, int color,
                       char *str, size_t len, int *delta_yscroll)
{
    /* presume we won't scroll */
    if (delta_yscroll != 0)
        *delta_yscroll = 0;

    /* keep going until we exhaust the string */
    while (len != 0)
    {
        size_t cur;
        unsigned char oldc;

        /* display as much as will fit on the current line before wrapping */
        cur = win->winx + win->wid - x;
        if (cur > len)
            cur = len;

        /* null-terminate the chunk, but save the original character */
        oldc = str[cur];
        str[cur] = '\0';

        /* display this chunk */
        ossdsp(y, x, color, (char *)str);

        /* restore the character where we put our null */
        str[cur] = oldc;

        /* move our string counters past this chunk */
        str += cur;
        len -= cur;

        /* advance the x position */
        x += cur;

        /* if we've reached the right edge of the window, wrap the line */
        if (x >= win->winx + (int)win->wid)
        {
            /* wrap to the left edge of the window */
            x = win->winx;

            /* advance to the next line */
            ++y;

            /* 
             *   if this puts us past the bottom of the window, and we're
             *   allowed to scroll the window, do so
             */
            if (y >= win->winy + (int)win->ht && delta_yscroll != 0)
            {
                /* scroll by one line */
                ossscr(win->winy, win->winx,
                       win->winy + win->ht - 1, win->winx + win->wid - 1,
                       win->oss_fillcolor);

                /* adjust the scroll position of the window */
                win->scrolly++;

                /* count the scrolling */
                ++(*delta_yscroll);

                /* move back a line */
                --y;
            }
        }
    }
}

/*
 *   Move the cursor left by the number of characters.  
 */
static void oss_gets_csrleft(osgen_txtwin_t *win, int *y, int *x, size_t len)
{
    for ( ; len != 0 ; --len)
    {
        /* move left one character, wrapping at the end of the line */
        if (--*x < win->base.winx)
        {
            /* move up a line */
            --*y;

            /* move to the end of the line */
            *x = win->base.winx + win->base.wid - 1;
        }
    }
}

/*
 *   Move the cursor right by the number of characters.  
 */
static void oss_gets_csrright(osgen_txtwin_t *win, int *y, int *x, size_t len)
{
    for ( ; len != 0 ; --len)
    {
        /* move right one character, wrapping at the end of the line */
        if (++*x >= win->base.winx + (int)win->base.wid)
        {
            /* move down a line */
            ++*y;

            /* move to the left column */
            *x = win->base.winx;
        }
    }
}

/*
 *   clear an area of the display formerly occupied by some input text 
 */
static void oss_gets_clear(osgen_txtwin_t *win, int y, int x, size_t len)
{
    /* if we don't even reach the left edge of the window, ignore it */
    if (x + (int)len <= win->base.winx)
        return;

    /* skip anything to the left of the window */
    if (x < win->base.winx)
    {
        /* deduct the unseen part from the length */
        len -= (win->base.winx - x);

        /* start at the left edge */
        x = win->base.winx;
    }

    /* clear one line at a time */
    while (len != 0)
    {
        size_t cur;

        /* calculate how much we have left to the right edge of the window */
        cur = win->base.winx + win->base.wid - x;

        /* limit the clearing to the requested length */
        if (cur > len)
            cur = len;

        /* clear this chunk */
        ossclr(y, x, y, x + cur - 1, win->base.oss_fillcolor);

        /* deduct this chunk from the remaining length */
        len -= cur;

        /* move to the start of the next line */
        x = win->base.winx;
        ++y;

        /* if we're past the bottom of the window, stop */
        if (y >= win->base.winy + (int)win->base.ht)
            break;
    }
}

/*
 *   Delete a character in the buffer, updating the display. 
 */
static void oss_gets_delchar(osgen_txtwin_t *win,
                             char *buf, char *p, char **eol, int x, int y)
{
    int color;
    
    /* get the oss color for the current text in the window */
    color = ossgetcolor(win->base.txtfg, win->base.txtbg,
                        win->base.txtattr, win->base.fillcolor);

    /* if the character is within the buffer, delete it */
    if (p < *eol)
    {
        /* delete the character and close the gap */
        --*eol;
        if (p != *eol)
            memmove(p, p + 1, *eol - p);

        /* null-terminate the shortened buffer */
        **eol = '\0';
        
        /* re-display the changed part of the string */
        ossdsp_str(&win->base, y, x, color, p, *eol - p, 0);

        /* move to the position of the former last character */
        oss_gets_csrright(win, &y, &x, *eol - p);

        /* clear the screen area where the old last character was displayed */
        ossclr(y, x, y, x, win->base.oss_fillcolor);
    }
}

/*
 *   Backspace in the buffer, updating the display and adjusting the cursor
 *   position. 
 */
static void oss_gets_backsp(osgen_txtwin_t *win,
                            char *buf, char **p,
                            char **eol, int *x, int *y)
{
    int color;

    /* get the oss color for the current text in the window */
    color = ossgetcolor(win->base.txtfg, win->base.txtbg,
                        win->base.txtattr, win->base.fillcolor);

    /* if we can back up, do so */
    if (*p > buf)
    {
        int tmpy;
        int tmpx;

        /* move our insertion point back one position */
        --*p;
        
        /* the line is now one character shorter */
        --*eol;

        /* shift all of the characters down one position */
        if (*p != *eol)
            memmove(*p, *p + 1, *eol - *p);

        /* move the cursor back, wrapping if at the first column */
        if (--*x < win->base.winx)
        {
            *x = win->base.winx + win->base.wid - 1;
            --*y;
        }

        /* null-terminate the shortened buffer */
        **eol = '\0';

        /* 
         *   display the string from the current position, so that we update
         *   the display for the moved characters 
         */
        tmpy = *y;
        tmpx = *x;
        ossdsp_str(&win->base, tmpy, tmpx, color, *p, *eol - *p, 0);

        /* clear the screen area where the old last character was shown */
        oss_gets_csrright(win, &tmpy, &tmpx, *eol - *p);
        ossclr(tmpy, tmpx, tmpy, tmpx, win->base.oss_fillcolor);
    }
}

/*
 *   Redraw any command line under construction 
 */
static void osgen_gets_redraw_cmdline(void)
{
    osgen_txtwin_t *win;
    int x, y;
    int color;

    /* 
     *   get the default window; if we don't have one, or there's no command
     *   line editing in progress, there's nothing to do 
     */
    if ((win = S_default_win) == 0 || !S_gets_in_progress)
        return;

    /* set up at the current cursor position */
    x = S_gets_x;
    y = S_gets_y;

    /* move to the start of the command */
    oss_gets_csrleft(win, &y, &x, S_gets_ofs);

    /* get the color of the command text */
    color = ossgetcolor(win->base.txtfg, win->base.txtbg,
                        win->base.txtattr, win->base.fillcolor);

    /* draw the command line */
    ossdsp_str(&win->base, y, x, color, S_gets_buf, strlen(S_gets_buf), 0);
}

/* ------------------------------------------------------------------------ */
/*
 *   cancel interrupted input 
 */
void os_gets_cancel(int reset)
{
    int x, y;

    /* if we're doing scrollback, cancel it */
    if (S_sbmode_win != 0)
        osgen_sb_mode_end();

    /* do any deferred redrawing */
    osssb_redraw_if_needed();

    /* 
     *   if we interrupted a previous line, apply display effects as though
     *   the user had pressed return 
     */
    if (S_gets_in_progress)
    {
        osgen_txtwin_t *win;

        /* use the main window */
        win = S_main_win;

        /* move to the end of the input line */
        x = S_gets_x;
        y = S_gets_y;
        oss_gets_csrright(win, &y, &x, strlen(S_gets_buf + S_gets_ofs));
        
        /* set the cursor to the new position */
        ossloc(y, x);
        
        /* copy the buffer to the screen save buffer, adding a newline */
        ossaddsb_input(win, (char *)S_gets_buf, TRUE);

        /* we no longer have an input in progress */
        S_gets_in_progress = FALSE;
    }
    
    /* if we're resetting, clear our saved buffer */
    if (reset)
        S_gets_buf[0] = '\0';
}

/* ------------------------------------------------------------------------ */
/*
 *   Initialize input line editing mode.  Returns true if we can
 *   successfully set up input line editing mode, false if not.  
 */
int os_gets_begin(size_t max_line_len)
{
    osgen_txtwin_t *win;

    /* if there's no default window, there's nothing we can do */
    if ((win = S_default_win) == 0)
        return FALSE;

    /* do any deferred redrawing */
    osssb_redraw_if_needed();

    /* if the cursor if off the screen vertically, scroll to show it */
    if (win->base.y >= win->base.scrolly + (int)win->base.ht)
        osgen_scroll_win_fwd(win, win->base.y
                             - (win->base.scrolly + win->base.ht) + 1);

    /* if we're horizontally scrolled, scroll to the left edge */
    if (win->base.scrollx != 0)
    {
        /* scroll to the left edge */
        win->base.scrollx = 0;

        /* redraw the window */
        osgen_redraw_win(&win->base);
    }

# ifdef USE_HISTORY
    /* allocate the history buffer if it's not already allocated */
    if (histbuf == 0)
    {
        histbuf = (char *)osmalloc(HISTBUFSIZE);
        histhead = histtail = histbuf;
        S_gets_curhist = histhead;
    }
# endif /* USE_HISTORY */

    /*
     *   If we have saved input state from a previous interrupted call,
     *   restore it now.  Otherwise, initialize everything.  
     */
    if (S_gets_buf[0] != '\0' || S_gets_in_progress)
    {
        /* 
         *   if we cancelled the previous input, we must re-display the
         *   buffer under construction, since we have displayed something
         *   else in between and have re-displayed the prompt 
         */
        if (!S_gets_in_progress)
        {
            int x;
            int y;
            int deltay;
            int color;

            /* set up at the window's output position, in screen coords */
            x = win->base.winx + win->base.x - win->base.scrollx;
            y = win->base.winy + win->base.y - win->base.scrolly;

            /* get the current color in the window */
            color = ossgetcolor(win->base.txtfg, win->base.txtbg,
                                win->base.txtattr, win->base.fillcolor);

            /* re-display the buffer */
            ossdsp_str(&win->base, y, x, color,
                       S_gets_buf, strlen(S_gets_buf), &deltay);

            /* adjust our y position for any scrolling we just did */
            y -= deltay;

            /* limit the initial offset to the available buffer length */
            if (S_gets_ofs > (int)max_line_len)
                S_gets_ofs = max_line_len;

            /* move back to the original insertion point */
            oss_gets_csrright(win, &y, &x, S_gets_ofs);

            /* note the current position as the new editing position */
            S_gets_x = x;
            S_gets_y = y;
        }
    }
    else
    {
        /* initialize our history recall pointer */
        S_gets_curhist = histhead;

        /* set up at the window's output position, in screen coords */
        S_gets_x = win->base.winx + win->base.x - win->base.scrollx;
        S_gets_y = win->base.winy + win->base.y - win->base.scrolly;

        /* we're at offset zero in the input line */
        S_gets_ofs = 0;
    }

    /* 
     *   set the buffer end pointer to limit input to the maximum size the
     *   caller has requested, or the maximum size of our internal buffer,
     *   whichever is smaller 
     */
    if (max_line_len > S_gets_buf_siz)
        S_gets_buf_end = S_gets_buf + S_gets_buf_siz - 1;
    else
        S_gets_buf_end = S_gets_buf + max_line_len - 1;

    /* override the default cursor position logic while reading */
    S_special_cursor_pos = TRUE;
    S_special_cursor_x = S_gets_x;
    S_special_cursor_y = S_gets_y;

    /* note that input is in progress */
    S_gets_in_progress = TRUE;

    /* successfully set up */
    return TRUE;
}


/* ------------------------------------------------------------------------ */
/*
 *   Process an event in input line editing mode.  Returns true if the user
 *   explicitly ended input line editing by pressing Return or something
 *   similar, false if input line editing mode remains active.  
 */
int os_gets_process(int event_type, os_event_info_t *event_info)
{
    unsigned char c;
    char *p;
    char *eol;
    char *buf;
    int x;
    int y;
    osgen_txtwin_t *win;
    int color;

    /* use the default window */
    if ((win = S_default_win) == 0)
        return TRUE;

    /* if it's a keystroke event, convert from "raw" to a CMD_xxx code */
    if (event_type == OS_EVT_KEY)
        oss_raw_key_to_cmd(event_info);

    /* 
     *   if we're in scrollback mode, run the event through the scrollback
     *   handler rather than handling it directly 
     */
    if (S_sbmode_win != 0)
    {
        /* run the event through scrollback mode */
        if (osgen_sb_mode(win, event_type, event_info))
        {
            /* 
             *   scrollback mode fully handled the event, so we're done;
             *   tell the caller we didn't finish command input editing 
             */
            return FALSE;
        }
    }

    /* ignore everything except keystroke events */
    if (event_type != OS_EVT_KEY)
        return FALSE;

    /* get the key from the event */
    c = (unsigned char)event_info->key[0];

    /* set up at the current position */
    x = S_gets_x;
    y = S_gets_y;

    /* set up our buffer pointers */
    p = S_gets_buf + S_gets_ofs;
    buf = S_gets_buf;
    eol = p + strlen(p);

    /* get the current color in the window */
    color = ossgetcolor(win->base.txtfg, win->base.txtbg,
                        win->base.txtattr, win->base.fillcolor);

    /* 
     *   Check the character we got.  Note that we must interpret certain
     *   control characters explicitly, because os_get_event() returns raw
     *   keycodes (untranslated into CMD_xxx codes) for control characters.  
     */
    switch(c)
    {
    case 8:
        /* backspace one character */
        oss_gets_backsp(win, buf, &p, &eol, &x, &y);
        break;
        
    case 13:
        /* Return/Enter key - we're done.  Null-terminate the input. */
        *eol = '\0';
        
        /* move to the end of the line */
        oss_gets_csrright(win, &y, &x, eol - p);
        p = eol;

# ifdef USE_HISTORY
        /*
         *   Save the line in our history buffer.  If we don't have enough
         *   room, lose some old text by advancing the tail pointer far
         *   enough.  Don't save it if it's a blank line, though, or if it
         *   duplicates the most recent previous command.  
         */
        if (strlen(buf) != 0)
        {
            char *q;
            int advtail;
            int saveflag = 1;                /* assume we will be saving it */
            
            if (q = ossprvcmd(histhead))
            {
                char *p = buf;
                
                while (*p == *q && *p != '\0' && *q != '\0')
                {
                    ++p;
                    q = ossadvhp(q);
                }
                if (*p == *q)               /* is this a duplicate command? */
                    saveflag = 0;                   /* if so, don't save it */
            }

            if (saveflag)
            {
                for (q = buf, advtail = 0 ; q <= eol ; ++q)
                {
                    *histhead = *q;
                    histhead = ossadvhp(histhead);
                    if (histhead == histtail)
                    {
                        histtail = ossadvhp(histtail);
                        advtail = 1;
                    }
                }

                /*
                 *   If we have encroached on space that was already
                 *   occupied, throw away the entire command we have
                 *   partially trashed; to do so, advance the tail pointer
                 *   to the next null byte.  
                 */
                if (advtail)
                {
                    while(*histtail)
                        histtail = ossadvhp(histtail);
                    histtail = ossadvhp(histtail);
                }
            }
        }
# endif /* USE_HISTORY */

        /* add the text to the scrollback buffer */
        ossaddsb_input(win, buf, TRUE);

        /* done with the special cursor position */
        S_special_cursor_pos = FALSE;

        /* 
         *   immediately scroll the window if necessary, and make sure the
         *   cursor is showing at the right location 
         */
        osgen_auto_vscroll(win);
        osssb_cursor_to_default_pos();

        /* input is no longer in progress */
        S_gets_in_progress = FALSE;

        /* tell the caller we're done */
        return TRUE;

    case 0:
        /* extended key code - get the second half of the code */
        c = (unsigned char)event_info->key[1];

        /* handle the command key code */
        switch(c)
        {
# ifdef USE_SCROLLBACK
        case CMD_SCR:
        case CMD_PGUP:
        case CMD_PGDN:
            /* run the event through scrollback mode */
            osgen_sb_mode(win, event_type, event_info);

            /* done */
            break;
# endif /* USE_SCROLLBACK */

        case CMD_LEFT:
            /* move the cursor left */
            if (p > buf)
            {
                --p;
                oss_gets_csrleft(win, &y, &x, 1);
            }
            break;

        case CMD_WORD_LEFT:
            /*
             *   Move back one word.  This moves the cursor back a
             *   character, then seeks back until we're on a non-space
             *   character, then seeks back until we're on a character
             *   preceded by a space character.  
             */
            if (p > buf)
            {
                /* back up one character */
                --p;
                oss_gets_csrleft(win, &y, &x, 1);

                /* back up until we're on a non-space character */
                while (p > buf && t_isspace(*p) && !t_isspace(*(p-1)))
                {
                    --p;
                    oss_gets_csrleft(win, &y, &x, 1);
                }

                /* 
                 *   back up again until we're on a character preceded by a
                 *   space character 
                 */
                while (p > buf && !t_isspace(*(p-1)))
                {
                    --p;
                    oss_gets_csrleft(win, &y, &x, 1);
                }
            }
            break;

        case CMD_RIGHT:
            /* move the cursor right */
            if (p < eol)
            {
                ++p;
                oss_gets_csrright(win, &y, &x, 1);
            }
            break;

        case CMD_WORD_RIGHT:
            /*
             *   Move right one word.  This moves the cursor right until
             *   we're on a space character, then moves the cursor right
             *   again until we're on a non-space character.  First, move
             *   right until we're between words (i.e., until we're on a
             *   space character).  
             */
            while (p < eol && !t_isspace(*p))
            {
                ++p;
                oss_gets_csrright(win, &y, &x, 1);
            }

            /* now move right until we're on a non-space character */
            while (p < eol && t_isspace(*p))
            {
                ++p;
                oss_gets_csrright(win, &y, &x, 1);
            }
            break;

        case CMD_DEL:
            /* delete a character */
            oss_gets_delchar(win, buf, p, &eol, x, y);
            break;

#ifdef UNIX               
        case CMD_WORDKILL:
            {
                /* remove spaces preceding word */
                while (p >= buf && *p <= ' ')
                    oss_gets_backsp(win, buf, &p, &eol, &x, &y);

                /* remove previous word (i.e., until we get a space) */
                while (p >= buf && *p > ' ')
                    oss_gets_backsp(win, buf, &p, &eol, &x, &y);

                /* that's it */
                break;
            }
#endif /* UNIX */

        case CMD_KILL:
        case CMD_HOME:
# ifdef USE_HISTORY
        case CMD_UP:
        case CMD_DOWN:
            /*
             *   Home, Kill (delete entire line), History Up, History Down -
             *   what these all have in common is that we move to the start
             *   of the line before doing anything else.  
             */

            /* if 'up', make sure we have more history to traverse */
            if (c == CMD_UP && !ossprvcmd(S_gets_curhist))
                break;

            /* if 'down', make sure there's more history to traverse */
            if (c == CMD_DOWN && !ossnxtcmd(S_gets_curhist))
                break;

            /* 
             *   if this is the first 'up', save the current buffer, so that
             *   we can reinstate it if we traverse back 'down' until we're
             *   back at the original buffer (the active buffer essentially
             *   becomes a temporary history entry that we can recover by
             *   history-scrolling back down to it) 
             */
                if (c == CMD_UP && !ossnxtcmd(S_gets_curhist))
                    strcpy(S_hist_sav, buf);

# endif /* USE_HISTORY */

                /* move to the start of the line */
                if (p != buf)
                {
                    /* move the cursor */
                    oss_gets_csrleft(win, &y, &x, p - buf);

                    /* move the insertion pointer */
                    p = buf;
                }

                /* if it was just a 'home' command, we're done */
                if (c == CMD_HOME)
                    break;

                /*
                 *   We're at the start of the line now; fall through for
                 *   KILL, UP, and DOWN to the code which deletes to the end
                 *   of the line.  
                 */

        case CMD_DEOL:
            /* clear the remainder of the line on the display */
            oss_gets_clear(win, y, x, eol - p);

            /* truncate the buffer at the insertion point */
            eol = p;
            *p = '\0';

# ifdef USE_HISTORY
            if (c == CMD_UP)
            {
                S_gets_curhist = ossprvcmd(S_gets_curhist);
                osshstcpy(buf, S_gets_curhist);
            }
            else if (c == CMD_DOWN)
            {
                if (!ossnxtcmd(S_gets_curhist))
                    break;                                       /* no more */
                S_gets_curhist = ossnxtcmd(S_gets_curhist);
                if (ossnxtcmd(S_gets_curhist))        /* on a valid command */
                    osshstcpy(buf, S_gets_curhist);        /* ... so use it */
                else
                {
                    /* no more history - restore original line */
                    strcpy(buf, S_hist_sav);
                }
            }
            if ((c == CMD_UP || c == CMD_DOWN)
                && strlen(buf) != 0)
            {
                int deltay;

                /* get the end pointer based on null termination */
                eol = buf + strlen(buf);

                /* display the string */
                ossdsp_str(&win->base, y, x, color, p, eol - p, &deltay);
                y -= deltay;

                /* move to the end of the line */
                oss_gets_csrright(win, &y, &x, eol - p);
                p = eol;
            }
# endif /* USE_HISTORY */
            break;
        case CMD_END:
            while (p < eol)
            {
                ++p;
                if (++x >= win->base.winx + (int)win->base.wid)
                {
                    x = win->base.winx;
                    ++y;
                }
            }
            break;
        }
        break;

    default:
        if (c >= ' ' && eol < S_gets_buf_end)
        {
            int deltay;

            /* open up the line and insert the character */
            if (p != eol)
                memmove(p + 1, p, eol - p);
            ++eol;
            *p = (char)c;
            *eol = '\0';

            /* write the updated part of the line */
            ossdsp_str(&win->base, y, x, color, p, eol - p, &deltay);
            y -= deltay;

            /* move the cursor right one character */
            oss_gets_csrright(win, &y, &x, 1);

            /* advance the buffer pointer one character */
            ++p;
        }
        break;
    }

    /* remember the current editing position */
    S_special_cursor_x = S_gets_x = x;
    S_special_cursor_y = S_gets_y = y;
    S_gets_ofs = p - S_gets_buf;

    /* we didn't finish editing */
    return FALSE;
}

/* ------------------------------------------------------------------------ */
/*
 *   Common routine to read a command from the keyboard.  This
 *   implementation provides command editing and history, as well as timeout
 *   capabilities.
 */
int os_gets_timeout(unsigned char *buf, size_t bufl,
                    unsigned long timeout, int use_timeout)
{
    long end_time;
    osgen_txtwin_t *win;
    
    /* if we're in 'plain' mode, simply use stdio input */
    if (os_f_plain)
    {
        size_t len;

        /* make sure the standard output is flushed */
        fflush(stdout);

        /* we don't support the timeout feature in plain mode */
        if (use_timeout)
            return OS_EVT_NOTIMEOUT;

        /* 
         *   get input from stdio, and translate the result code - if gets()
         *   returns null, it indicates an error of some kind, so return an
         *   end-of-file indication 
         */
        if (fgets((char *)buf, bufl, stdin) == 0)
            return OS_EVT_EOF;

        /* remove the trailing newline from the buffer, if present */
        if ((len = strlen((char *)buf)) != 0 && buf[len-1] == '\n')
            buf[len-1] = '\0';

        /* indicate that we read a line */
        return OS_EVT_LINE;
    }

    /* begin input editing mode */
    os_gets_begin(bufl);

    /*
     *   If we have a timeout, calculate the system clock time at which the
     *   timeout expires.  This is simply the current system clock time plus
     *   the timeout interval.  Since we might need to process a series of
     *   events, we'll need to know how much time remains at each point we
     *   get a new event.  
     */
    end_time = os_get_sys_clock_ms() + timeout;
    
    /* use the default window for input */
    if ((win = S_default_win) == 0)
        return OS_EVT_EOF;

    /* process keystrokes until we're done entering the command */
    for ( ;; )
    {
        int event_type;
        os_event_info_t event_info;
        
        /* if we're using a timeout, check for expiration */
        if (use_timeout)
        {
            long cur_clock;

            /* note the current system clock time */
            cur_clock = os_get_sys_clock_ms();

            /* 
             *   if we're past the timeout expiration time already,
             *   interrupt with timeout now 
             */
            if (cur_clock >= end_time)
                goto timeout_expired;

            /* note the interval remaining to the timeout expiration */
            timeout = end_time - cur_clock;
        }
        
        /* move to the proper position on the screen before pausing */
        if (S_sbmode_win != 0)
            ossloc(S_sbmode_win->base.winy, S_sbmode_win->base.winx);
        else
            ossloc(S_gets_y, S_gets_x);

        /* get an event */
        event_type = os_get_event(timeout, use_timeout, &event_info);

        /* handle the event according to the event type */
        switch(event_type)
        {
        case OS_EVT_TIMEOUT:
        timeout_expired:
            /* done with the overridden cursor position */
            S_special_cursor_pos = FALSE;

            /* return the timeout status to the caller */
            return OS_EVT_TIMEOUT;

        case OS_EVT_NOTIMEOUT:
            /* 
             *   we can't handle events with timeouts, so we can't provide
             *   line reading with timeouts, either
             */
            S_special_cursor_pos = FALSE;
            return OS_EVT_NOTIMEOUT;

        case OS_EVT_EOF:
            /* end of file - end input and return the EOF to our caller */
            S_special_cursor_pos = FALSE;
            S_gets_in_progress = FALSE;
            return OS_EVT_EOF;

        default:
            /* process anything else through the input line editor */
            if (os_gets_process(event_type, &event_info))
            {
                /* 
                 *   Copy the result to the caller's buffer.  Note that we
                 *   know the result will fit, because we always limit the
                 *   editing process to the caller's buffer size.  
                 */
                strcpy((char *)buf, S_gets_buf);

                /* clear the input buffer */
                S_gets_buf[0] = '\0';

                /* input is no longer in progress */
                S_gets_in_progress = FALSE;

                /* return success */
                return OS_EVT_LINE;
            }
            break;
        }
    }
}

/*
 *   Read a line of input.  We implement this in terms of the timeout input
 *   line reader, passing an infinite timeout to that routine.
 */
uchar *os_gets(unsigned char *buf, size_t bufl)
{
    int evt;

    /* cancel any previous input, clearing the buffer */
    os_gets_cancel(TRUE);

    /* get a line of input, with no timeout */
    evt = os_gets_timeout(buf, bufl, 0, FALSE);

    /* translate the event code to the appropriate return value */
    switch(evt)
    {
    case OS_EVT_LINE:
        /* we got a line of input - return a pointer to our buffer */
        return buf;

    case OS_EVT_EOF:
        /* end of file - return null */
        return 0;

    default:
        /* we don't expect any other results */
        assert(FALSE);
        return 0;
    }
}

#else /* USE_STATLINE */

#endif /* USE_STATLINE */

/* ------------------------------------------------------------------------ */
/*
 *   Highlighting and colors 
 */

#ifdef STD_OS_HILITE

#ifdef RUNTIME
/*
 *   Set text attributes 
 */
void os_set_text_attr(int attr)
{
    osgen_txtwin_t *win;

    /* if there's no default output window, do nothing */
    if ((win = S_default_win) == 0)
        return;

    /* 
     *   if the attributes are different from the old attributes, add an
     *   attribute-change sequence to the display buffer 
     */
    if (attr != win->base.txtattr)
    {
        char buf[3];

        /* set up the attribute-change sequence */
        buf[0] = OSGEN_ATTR;
        buf[1] = (char)attr;
        ossaddsb(win, buf, 2, TRUE);
    }
}

/*
 *   Translate a color from the os_color_t encoding to an OSGEN_xxx color.  
 */
static char osgen_xlat_color_t(os_color_t color)
{
    size_t i;
    struct color_map_t
    {
        /* the OSGEN_COLOR_xxx value */
        char id;

        /* the RGB components for the color */
        unsigned char rgb[3];
    };
    struct color_map_t *p;
    struct color_map_t *bestp;
    static struct color_map_t color_map[] =
    {
        { OSGEN_COLOR_BLACK,   { 0x00, 0x00, 0x00 } },
        { OSGEN_COLOR_WHITE,   { 0xFF, 0xFF, 0xFF } },
        { OSGEN_COLOR_RED,     { 0xFF, 0x00, 0x00 } },
        { OSGEN_COLOR_BLUE,    { 0x00, 0x00, 0xFF } },
        { OSGEN_COLOR_GREEN,   { 0x00, 0x80, 0x00 } },
        { OSGEN_COLOR_YELLOW,  { 0xFF, 0xFF, 0x00 } },
        { OSGEN_COLOR_CYAN,    { 0x00, 0xFF, 0xFF } },
        { OSGEN_COLOR_SILVER,  { 0xC0, 0xC0, 0xC0 } },
        { OSGEN_COLOR_GRAY,    { 0x80, 0x80, 0x80 } },
        { OSGEN_COLOR_MAROON,  { 0x80, 0x00, 0x00 } },
        { OSGEN_COLOR_PURPLE,  { 0x80, 0x00, 0x80 } },
        { OSGEN_COLOR_MAGENTA, { 0xFF, 0x00, 0xFF } },
        { OSGEN_COLOR_LIME,    { 0x00, 0xFF, 0x00 } },
        { OSGEN_COLOR_OLIVE,   { 0x80, 0x80, 0x00 } },
        { OSGEN_COLOR_NAVY,    { 0x00, 0x00, 0x80 } },
        { OSGEN_COLOR_TEAL,    { 0x00, 0x80, 0x80 } }
    };
    unsigned char r, g, b;
    unsigned long best_dist;

    /* 
     *   If it's parameterized, map it by shifting the parameter code (in
     *   the high-order 8 bits of the os_color_t) to our single-byte code,
     *   which is defined as exactly the same code as the os_color_t values
     *   but shifted into the low-order 8 bits.  
     */
    if (os_color_is_param(color))
        return (char)((color >> 24) & 0xFF);

    /* break the color into its components */
    r = os_color_get_r(color);
    g = os_color_get_g(color);
    b = os_color_get_b(color);

    /* search for the closest match among our 16 ANSI colors */
    for (i = 0, p = color_map, bestp = 0, best_dist = 0xFFFFFFFF ;
         i < sizeof(color_map)/sizeof(color_map[0]) ; ++i, ++p)
    {
        unsigned long dist;
        int rd, gd, bd;

        /* calculate the delta for each component */
        rd = r - p->rgb[0];
        gd = g - p->rgb[1];
        bd = b - p->rgb[2];

        /* calculate the "distance" in RGB space */
        dist = rd*rd + gd*gd + bd*bd;

        /* if it's an exact match, we need look no further */
        if (dist == 0)
            return p->id;

        /* if it's the smallest distance so far, note it */
        if (dist < best_dist)
        {
            best_dist = dist;
            bestp = p;
        }
    }

    /* return the OSGEN_COLOR_xxx ID of the best match we found */
    return bestp->id;
}

/*
 *   Set the text colors.
 *   
 *   The foreground and background colors apply to subsequent characters
 *   displayed via os_print().  If the background color is set to zero, it
 *   indicates "transparent" drawing: subsequent text is displayed with the
 *   "screen" color.  
 */
void os_set_text_color(os_color_t fg, os_color_t bg)
{
    char buf[4];

    /* if we're in plain mode, ignore it */
    if (os_f_plain || S_default_win == 0)
        return;

    /* add the color sequence to the default window's scrollback buffer */
    buf[0] = OSGEN_COLOR;
    buf[1] = osgen_xlat_color_t(fg);
    buf[2] = osgen_xlat_color_t(bg);
    ossaddsb(S_default_win, buf, 3, TRUE);
}

/*
 *   Set the screen color 
 */
void os_set_screen_color(os_color_t color)
{
    /* if we're in plain mode, ignore it */
    if (os_f_plain || S_default_win == 0)
        return;

    /* set the new background color in the default buffer */
    S_default_win->base.fillcolor = osgen_xlat_color_t(color);
    S_default_win->base.oss_fillcolor =
        ossgetcolor(OSGEN_COLOR_TEXT, osgen_xlat_color_t(color), 0, 0);

    /* redraw the window if we don't have a scheduled redraw already */
    if (!S_deferred_redraw
        && !(S_default_win->base.flags & OSGEN_DEFER_REDRAW))
        osgen_redraw_win(&S_default_win->base);
}

/* ------------------------------------------------------------------------ */
/*
 *   Banners 
 */

/*
 *   create a banner window 
 */
void *os_banner_create(void *parent, int where, void *other, int wintype,
                       int align, int siz, int siz_units, unsigned long style)
{
    osgen_win_t *win;

    /* we don't support banners in plain mode */
    if (os_f_plain)
        return 0;

    /* if the parent is null, it means that it's a child of the main window */
    if (parent == 0)
        parent = &S_main_win->base;

    /* check for a supported window type */
    switch(wintype)
    {
    case OS_BANNER_TYPE_TEXT:
        /* 
         *   Create a text window.  We don't support scrollback in the UI in
         *   banner windows, so we only need enough for what's on the screen
         *   for redrawing.  Overallocate by a bit, though, to be safe in
         *   case the screen grows later.  
         */
        win = (osgen_win_t *)osgen_create_txtwin(where, other, parent,
            G_oss_screen_height * G_oss_screen_width * 2,
            G_oss_screen_height * 2);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /*
         *   Create a text grid window.  Make it ten lines high at the
         *   current screen width; we'll automatically expand this
         *   allocation as needed later, so this size doesn't have to be a
         *   perfect guess; but the closer we get the better, as it's more
         *   efficient to avoid reallocating if possible.  
         */
        win = (osgen_win_t *)osgen_create_gridwin(where, other, parent,
            G_oss_screen_width, 10);
        break;

    default:
        /* unsupported type - return failure */
        return 0;
    }
    
    /* if that failed, return null */
    if (win == 0)
        return 0;

    /* set the alignment */
    win->alignment = align;

    /*
     *   Start out width a zero size in the settable dimension, and the
     *   current main text area size in the constrained dimension. 
     */
    if (align == OS_BANNER_ALIGN_LEFT || align == OS_BANNER_ALIGN_RIGHT)
    {
        /* the width is the settable dimension for a left/right banner */
        win->wid = 0;
        win->ht = (S_main_win != 0
                   ? S_main_win->base.ht : G_oss_screen_height);
    }
    else
    {
        /* the height is the settable dimension for a top/bottom banner */
        win->wid = (S_main_win != 0
                    ? S_main_win->base.wid : G_oss_screen_width);
        win->ht = 0;
    }

    /* set auto-vscroll mode if they want it */
    if ((style & OS_BANNER_STYLE_AUTO_VSCROLL) != 0)
        win->flags |= OSGEN_AUTO_VSCROLL;

    /* 
     *   Note the MORE mode style, if specified.  MORE mode implies
     *   auto-vscroll, so add that style as well if MORE mode is requested.  
     */
    if ((style & OS_BANNER_STYLE_MOREMODE) != 0)
        win->flags |= OSGEN_AUTO_VSCROLL | OSGEN_MOREMODE;

    /* note the "strut" style flags, if specified */
    if ((style & OS_BANNER_STYLE_VSTRUT) != 0)
        win->flags |= OSGEN_VSTRUT;
    if ((style & OS_BANNER_STYLE_HSTRUT) != 0)
        win->flags |= OSGEN_HSTRUT;

    /* remember the requested size */
    win->size = siz;
    win->size_type = siz_units;

    /* 
     *   if the window has a non-zero size, recalculate the layout; if the
     *   size is zero, we don't have to bother, since the layout won't affect
     *   anything on the display 
     */
    if (siz != 0)
        osgen_recalc_layout();

    /* return the window */
    return win;
}

/*
 *   delete a banner 
 */
void os_banner_delete(void *banner_handle)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* delete the window */
    osgen_delete_win(win);

    /* recalculate the display layout */
    osgen_recalc_layout();
}

/*
 *   orphan a banner - treat this exactly like delete 
 */
void os_banner_orphan(void *banner_handle)
{
    os_banner_delete(banner_handle);
}

/*
 *   get information on the banner 
 */
int os_banner_getinfo(void *banner_handle, os_banner_info_t *info)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* set the alignment */
    info->align = win->alignment;

    /* set the flags */
    info->style = 0;
    if ((win->flags & OSGEN_AUTO_VSCROLL) != 0)
        info->style |= OS_BANNER_STYLE_AUTO_VSCROLL;
    if ((win->flags & OSGEN_MOREMODE) != 0)
        info->style |= OS_BANNER_STYLE_MOREMODE;
    if ((win->flags & OSGEN_HSTRUT) != 0)
        info->style |= OS_BANNER_STYLE_HSTRUT;
    if ((win->flags & OSGEN_VSTRUT) != 0)
        info->style |= OS_BANNER_STYLE_VSTRUT;

    /* set the character size */
    info->rows = win->ht;
    info->columns = win->wid;

    /* we're a character-mode platform, so we don't have a pixel size */
    info->pix_width = 0;
    info->pix_height = 0;

    /* 
     *   We are designed for fixed-pitch character-mode displays only, so we
     *   support <TAB> alignment by virtue of our fixed pitch.
     */
    info->style |= OS_BANNER_STYLE_TAB_ALIGN;

    /* we do not do our own line wrapping */
    info->os_line_wrap = FALSE;

    /* indicate success */
    return TRUE;
}

/*
 *   clear the contents of a banner 
 */
void os_banner_clear(void *banner_handle)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* clear the window */
    osgen_clear_win(win);
}

/*
 *   display text in a banner 
 */
void os_banner_disp(void *banner_handle, const char *txt, size_t len)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* write the text according to the window type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /* normal text window - write the text into the scrollback buffer */
        ossaddsb((osgen_txtwin_t *)win, txt, len, TRUE);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* text grid - write the text into the grid */
        osgen_gridwin_write((osgen_gridwin_t *)win, txt, len);
        break;
    }
}

/*
 *   set the text attributes in a banner 
 */
void os_banner_set_attr(void *banner_handle, int attr)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;
    char buf[3];

    /* if the attributes aren't changing, ignore it */
    if (attr == win->txtattr)
        return;

    /* set the attribute according to the window type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /* add the color sequence to the window's scrollback buffer */
        buf[0] = OSGEN_ATTR;
        buf[1] = (char)attr;
        ossaddsb((osgen_txtwin_t *)win, buf, 2, TRUE);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* text grid windows don't use attributes - ignore it */
        break;
    }
}

/*
 *   set the text color in a banner
 */
void os_banner_set_color(void *banner_handle, os_color_t fg, os_color_t bg)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;
    char buf[4];

    /* set the color according to the window type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXT:
        /* add the color sequence to the window's scrollback buffer */
        buf[0] = OSGEN_COLOR;
        buf[1] = osgen_xlat_color_t(fg);
        buf[2] = osgen_xlat_color_t(bg);
        ossaddsb((osgen_txtwin_t *)win, buf, 3, TRUE);
        break;

    case OS_BANNER_TYPE_TEXTGRID:
        /* simply set the current color in the window */
        win->txtfg = osgen_xlat_color_t(fg);
        win->txtbg = osgen_xlat_color_t(bg);
        break;
    }
}

/*
 *   set the window color in a banner 
 */
void os_banner_set_screen_color(void *banner_handle, os_color_t color)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* set the new background color in the window */
    win->fillcolor = osgen_xlat_color_t(color);
    win->oss_fillcolor = ossgetcolor(OSGEN_COLOR_TEXT, win->fillcolor, 0, 0);

    /* redraw the window if we don't have a redraw scheduled already */
    if (!S_deferred_redraw && !(win->flags & OSGEN_DEFER_REDRAW))
        osgen_redraw_win(win);
}

/*
 *   flush text in a banner 
 */
void os_banner_flush(void *banner_handle)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* if we're deferring redrawing, redraw now */
    osgen_redraw_win_if_needed(FALSE, win);
}

/*
 *   set the size 
 */
void os_banner_set_size(void *banner_handle, int siz, int siz_units,
                        int is_advisory)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* if the size isn't changing, do nothing */
    if (win->size == siz && win->size_type == siz_units)
        return;

    /* 
     *   if the size is only advisory, ignore it, since we do implement
     *   size-to-contents 
     */
    if (is_advisory)
        return;

    /* set the new size */
    win->size = siz;
    win->size_type = siz_units;

    /* recalculate the layout */
    osgen_recalc_layout();
}

/*
 *   calculate the content height, for os_banner_size_to_contents()
 */
static size_t oss_get_content_height(osgen_win_t *win)
{
    size_t y;
    osgen_win_t *chi;
    
    /* start with our own maximum 'y' size */
    y = win->ymax + 1;

    /* scan children for vertical strut styles, and include any we find */
    for (chi = win->first_child ; chi != 0 ; chi = chi->nxt)
    {
        /* if this is a vertical strut, include its height as well */
        if ((chi->flags & OSGEN_VSTRUT) != 0)
        {
            /* calculate the child height */
            size_t chi_y = oss_get_content_height(chi);

            /* 
             *   if the child is horizontal, add its height to the parent's;
             *   otherwise, it shares the same height, so use the larger of
             *   the parent's natural height or the child's natural height 
             */
            if (chi->alignment == OS_BANNER_ALIGN_TOP
                || chi->alignment == OS_BANNER_ALIGN_BOTTOM)
            {
                /* it's horizontal - add its height to the parent's */
                y += chi_y;
            }
            else
            {
                /* it's vertical - they share a common height */
                if (y < chi_y)
                    y = chi_y;
            }
        }
    }

    /* return the result */
    return y;
}

/*
 *   calculate the content width, for os_banner_size_to_contents() 
 */
static size_t oss_get_content_width(osgen_win_t *win)
{
    size_t x;
    osgen_win_t *chi;
    
    /* start with the maximum 'x' size we've seen in the window */
    x = win->xmax + 1;

    /* scan children for horizontal strut styles, and include any we find */
    for (chi = win->first_child ; chi != 0 ; chi = chi->nxt)
    {
        /* if this is a horizontal strut, include its height as well */
        if ((chi->flags & OSGEN_HSTRUT) != 0)
        {
            /* calculate the child width */
            size_t chi_x = oss_get_content_width(chi);

            /* 
             *   if the child is vertical, add its width to the parent's;
             *   otherwise, it shares the same width, so use the larger of
             *   the parent's natural width or the child's natural width 
             */
            if (chi->alignment == OS_BANNER_ALIGN_LEFT
                || chi->alignment == OS_BANNER_ALIGN_RIGHT)
            {
                /* it's vertical - add its width to the parent's */
                x += chi_x;
            }
            else
            {
                /* it's horizontal - they share a common width */
                if (x < chi_x)
                    x = chi_x;
            }
        }
    }

    /* return the result */
    return x;
}

/*
 *   size a banner to its contents 
 */
void os_banner_size_to_contents(void *banner_handle)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* the sizing depends on the window's alignment */
    if (win->alignment == OS_BANNER_ALIGN_TOP
        || win->alignment == OS_BANNER_ALIGN_BOTTOM)
    {
        size_t newy;

        /* calculate the new height */
        newy = oss_get_content_height(win);

        /* 
         *   if this is the same as the current height, there's no need to
         *   redraw anything 
         */
        if (win->ht == newy)
            return;

        /* set the new size as a fixed character-cell size */
        win->size = newy;
        win->size_type = OS_BANNER_SIZE_ABS;
    }
    else
    {
        size_t newx;

        /* calculate the new width */
        newx = oss_get_content_width(win);

        /* if the size isn't changing, there's no need to redraw */
        if (win->wid == newx)
            return;

        /* set the new size as a fixed character-cell size */
        win->size = newx;
        win->size_type = OS_BANNER_SIZE_ABS;
    }

    /* recalculate the window layout */
    osgen_recalc_layout();
}

/*
 *   get the width, in characters, of the banner window 
 */
int os_banner_get_charwidth(void *banner_handle)
{
    /* return the current width from the window */
    return ((osgen_win_t *)banner_handle)->wid;
}

/*
 *   get the height, in characters, of the banner window 
 */
int os_banner_get_charheight(void *banner_handle)
{
    /* return the current height from the window */
    return ((osgen_win_t *)banner_handle)->ht;
}

/*
 *   start HTML mode in a banner 
 */
void os_banner_start_html(void *banner_handle)
{
    /* we don't support HTML mode, so there's nothing to do */
}

/*
 *   end HTML mode in a banner 
 */
void os_banner_end_html(void *banner_handle)
{
    /* we don't support HTML mode, so there's nothing to do */
}

/*
 *   set the output position in a text grid window 
 */
void os_banner_goto(void *banner_handle, int row, int col)
{
    osgen_win_t *win = (osgen_win_t *)banner_handle;

    /* check the window type */
    switch(win->win_type)
    {
    case OS_BANNER_TYPE_TEXTGRID:
        /* it's a text grid - move the output position */
        win->y = row;
        win->x = col;
        break;

    default:
        /* 
         *   this operation is meaningless with other window types - simply
         *   ignore it 
         */
        break;
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Non-RUNTIME version 
 */
#else /* RUNTIME */

void os_set_text_attr(int attr)
{
    /* attributes aren't supported in non-RUNTIME mode - ignore it */
}

void os_set_text_color(os_color_t fg, os_color_t bg)
{
    /* colors aren't supported in non-RUNTIME mode - ignore it */
}

void os_set_screen_color(os_color_t color)
{
    /* colors aren't supported in non-RUNTIME mode - ignore it */
}

/*
 *   Banners aren't supported in plain mode 
 */

void *os_banner_create(void *parent, int where, void *other, int wintype,
                       int align, int siz, int siz_units, unsigned long style)
{
    return 0;
}

void os_banner_delete(void *banner_handle)
{
}

void os_banner_orphan(void *banner_handle)
{
}

int os_banner_getinfo(void *banner_handle, os_banner_info_t *info)
{
    return FALSE;
}

void os_banner_clear(void *banner_handle)
{
}

int os_banner_get_charwidth(void *banner_handle)
{
    return 0;
}

int os_banner_get_charheight(void *banner_handle)
{
    return 0;
}

void os_banner_disp(void *banner_handle, const char *txt, size_t len)
{
}

void os_banner_set_color(void *banner_handle, os_color_t fg, os_color_t bg)
{
}

void os_banner_set_screen_color(void *banner_handle, os_color_t color)
{
}

void os_banner_flush(void *banner_handle)
{
}

void os_banner_set_size(void *banner_handle, int siz, int siz_units,
                        int is_advisory)
{
}

void os_banner_size_to_contents(void *banner_handle)
{
}

void os_banner_start_html(void *banner_handle)
{
}

void os_banner_end_html(void *banner_handle)
{
}

void os_banner_goto(void *banner_handle, int row, int col)
{
}

#endif /* RUNTIME */

#endif /* STD_OS_HILITE */

/* ------------------------------------------------------------------------ */
/* 
 *   clear the screen, deleting all scrollback information
 */
#ifdef STD_OSCLS

void oscls(void)
{
#ifdef RUNTIME
    /* do nothing in 'plain' mode */
    if (os_f_plain || S_default_win == 0)
        return;

    /* clear the default window */
    osgen_clear_win(&S_default_win->base);
#endif
}

#endif /* STD_OSCLS */

/* ------------------------------------------------------------------------ */
/*
 *   Simple implementation of os_get_sysinfo.  This can be used for any
 *   non-HTML version of the system, since all sysinfo codes currently
 *   pertain to HTML features.  Note that new sysinfo codes may be added
 *   in the future which may be relevant to non-html versions, so the
 *   sysinfo codes should be checked from time to time to ensure that new
 *   codes relevant to this system version are handled correctly here.  
 */
int os_get_sysinfo(int code, void *param, long *result)
{
#ifdef RUNTIME
    /* if the oss layer recognizes the code, defer to its judgment */
    if (oss_get_sysinfo(code, param, result))
        return TRUE;
#endif

    /* check the type of information they're requesting */
    switch(code)
    {
    case SYSINFO_HTML:
    case SYSINFO_JPEG:
    case SYSINFO_PNG:
    case SYSINFO_WAV:
    case SYSINFO_MIDI:
    case SYSINFO_WAV_MIDI_OVL:
    case SYSINFO_WAV_OVL:
    case SYSINFO_MPEG:
    case SYSINFO_MPEG1:
    case SYSINFO_MPEG2:
    case SYSINFO_MPEG3:
    case SYSINFO_PREF_IMAGES:
    case SYSINFO_PREF_SOUNDS:
    case SYSINFO_PREF_MUSIC:
    case SYSINFO_PREF_LINKS:
    case SYSINFO_LINKS_HTTP:
    case SYSINFO_LINKS_FTP:
    case SYSINFO_LINKS_NEWS:
    case SYSINFO_LINKS_MAILTO:
    case SYSINFO_LINKS_TELNET:
    case SYSINFO_PNG_TRANS:
    case SYSINFO_PNG_ALPHA:
    case SYSINFO_OGG:
    case SYSINFO_MNG:
    case SYSINFO_MNG_TRANS:
    case SYSINFO_MNG_ALPHA:
        /* 
         *   we don't support any of these features - set the result to 0
         *   to indicate this 
         */
        *result = 0;

        /* return true to indicate that we recognized the code */
        return TRUE;

    case SYSINFO_INTERP_CLASS:
        /* we're a text-only character-mode interpreter */
        *result = SYSINFO_ICLASS_TEXT;
        return TRUE;

#ifdef RUNTIME

    case SYSINFO_BANNERS:
        /* 
         *   we support the os_banner_xxx() interfaces, as long as we're not
         *   in "plain" mode 
         */
        *result = !os_f_plain;
        return TRUE;

#endif /* RUNTIME */

    default:
        /* not recognized */
        return FALSE;
    }
}

/* ------------------------------------------------------------------------ */
/*
 *   Set the saved-game extension.  Most platforms don't need to do
 *   anything with this information, and in fact most platforms won't even
 *   have a way of letting the game author set the saved game extension,
 *   so this trivial implementation is suitable for most systems.
 *   
 *   The purpose of setting a saved game extension is to support platforms
 *   (such as Windows) where the filename suffix is used to associate
 *   document files with applications.  Each stand-alone executable
 *   generated on such platforms must have a unique saved game extension,
 *   so that the system can associate each game's saved position files
 *   with that game's executable.  
 */
void os_set_save_ext(const char *ext)
{
    /* ignore the setting */
}


/* ------------------------------------------------------------------------ */
/*
 *   Set the game title.  Most platforms have no use for this information,
 *   so they'll just ignore it.  This trivial implementation simply
 *   ignores the title. 
 */
#ifdef USE_NULL_SET_TITLE

void os_set_title(const char *title)
{
    /* ignore the information */
}

#endif /* USE_NULL_SET_TITLE */

