/*
 * nvidia-installer: A tool for installing NVIDIA software packages on
 * Unix and Linux systems.
 *
 * Copyright (C) 2003 NVIDIA Corporation
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 *
 *
 * ncurses_ui.c - implementation of the nvidia-installer ui using ncurses.
 */

#include <ncurses.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>

#include "nvidia-installer.h"
#include "nvidia-installer-ui.h"




/* structures */

/*
 * RegionStruct - a region is sort of like an ncurses window, but
 * implemented completely in this source file (ie: doesn't use ncurses
 * windows).  I found enough bugs with ncurses windows that I felt it
 * better to avoid their use altogether.
 */

typedef struct {
    int x, y, w, h; /* position and dimensions relative to stdscr */
    int attr;       /* attributes (to be passed to setattr()) */
    char *line;     /* a string filled with spaces; its length is the
                       width of the region.  This is just a
                       convenience for use when clearing the region */
} RegionStruct;




/*
 * PagerStruct - Pager implements the functionality of `less`
 */

typedef struct {
    TextRows *t;          /* rows of text to be displayed */
    RegionStruct *region; /* region in which the text will be displayed */
    const char *label;    /* label for the left side of the footer */
    int cur;              /* current position in the pager */
    int page;             /* height of a page (for use with pgup/pgdn) */
} PagerStruct;




/*
 * DataStruct - private data structure that gets plugged into
 * Options->ui_priv.
 */

typedef struct {

    RegionStruct *header;  /* header region */
    RegionStruct *footer;  /* footer region */
    RegionStruct *message; /* message region */

    FormatTextRows format_text_rows; /* XXX function pointer from installer */

    bool use_color;        /* should the ncurses ui use color? */

    int width;             /* cached copy of the terminal dimensions */
    int height;

    char *title;           /* cached strings for the header and footer */
    char *footer_left;
    char *footer_right;
    
    char *progress_title;  /* cached string for the title of the
                              progress messages */
    
} DataStruct;




/* constants */

/* default strings for the header and footer */

#define NV_NCURSES_DEFAULT_TITLE "NVIDIA Software Installer for Unix/Linux"
#define NV_NCURSES_DEFAULT_FOOTER_LEFT NV_NCURSES_DEFAULT_TITLE
#define NV_NCURSES_DEFAULT_FOOTER_RIGHT "www.nvidia.com"

/* indices for color pairs */

#define NV_NCURSES_HEADER_COLOR_IDX      1
#define NV_NCURSES_MESSAGE_COLOR_IDX     2
#define NV_NCURSES_BUTTON_COLOR_IDX      3
#define NV_NCURSES_INPUT_COLOR_IDX       4


#define NV_NCURSES_HEADER_COLOR (COLOR_PAIR(NV_NCURSES_HEADER_COLOR_IDX))
#define NV_NCURSES_FOOTER_COLOR A_REVERSE
#define NV_NCURSES_MESSAGE_COLOR (COLOR_PAIR(NV_NCURSES_MESSAGE_COLOR_IDX))
#define NV_NCURSES_BUTTON_COLOR (COLOR_PAIR(NV_NCURSES_BUTTON_COLOR_IDX))
#define NV_NCURSES_INPUT_COLOR (COLOR_PAIR(NV_NCURSES_INPUT_COLOR_IDX))

#define NV_NCURSES_HEADER_NO_COLOR A_REVERSE
#define NV_NCURSES_FOOTER_NO_COLOR A_REVERSE
#define NV_NCURSES_MESSAGE_NO_COLOR A_NORMAL

/* use when animating button presses */

#define NV_NCURSES_BUTTON_PRESS_TIME 125000

#define NV_NCURSES_TAB 9
#define NV_NCURSES_ENTER 10
#define NV_NCURSES_BACKSPACE 8

#define NV_NCURSES_CTRL(x) ((x) & 0x1f)


/*
 * somewhat arbitrary minimum values: if the current window size is
 * smaller than this, don't attempt to use the ncurses ui.
 */

#define NV_NCURSES_MIN_WIDTH 40
#define NV_NCURSES_MIN_HEIGHT 10

#define NV_NCURSES_HLINE    '_'



/* prototypes for ui entry points */

static int   nv_ncurses_detect              (Options*);
static int   nv_ncurses_init                (Options*,
                                             FormatTextRows format_text_rows);
static void  nv_ncurses_set_title           (Options*, const char*);
static char *nv_ncurses_get_input           (Options*, const char*,
                                             const char*);
static void  nv_ncurses_message             (Options*, const int level,
                                             const char*);
static void  nv_ncurses_command_output      (Options*, const char*);
static int   nv_ncurses_approve_command_list(Options*, CommandList*,
                                             const char*);
static int   nv_ncurses_yes_no              (Options*, const int, const char*);
static int   nv_ncurses_multiple_choice     (Options *, const char*,
                                             const char * const *, int, int);
static void  nv_ncurses_status_begin        (Options*, const char*,
                                             const char*);
static void  nv_ncurses_status_update       (Options*, const float,
                                             const char*);
static void  nv_ncurses_status_end          (Options*, const char*);
static void  nv_ncurses_close               (Options*);



/* helper functions for manipulating the header and footer */

static void nv_ncurses_set_header(DataStruct *, const char *);
static void nv_ncurses_set_footer(DataStruct *, const char *, const char *);


/* helper functions for manipulating RegionStructs */

static RegionStruct *nv_ncurses_create_region(DataStruct *, int, int,
                                              int, int, int, int);
static void nv_ncurses_clear_region(RegionStruct *);
static void nv_ncurses_destroy_region(RegionStruct *);


/* helper functions for drawing buttons */

static void nv_ncurses_draw_button(DataStruct *, RegionStruct *, int, int,
                                   int, int, const char *, bool, bool);
static void nv_ncurses_erase_button(RegionStruct *, int, int, int, int);
static void draw_buttons(DataStruct *d, const char * const *buttons,
                         int num_buttons, int button, int button_w,
                         const int *buttons_x, int button_y);


/* helper functions for drawing regions and stuff */

static void nv_ncurses_do_message_region(DataStruct *, const char *,
                                         const char *, int, int);
static void nv_ncurses_do_progress_bar_region(DataStruct *);
static void nv_ncurses_do_progress_bar_message(DataStruct *, const char *,
                                               int, int);

/* pager functions */

static PagerStruct *nv_ncurses_create_pager(DataStruct *, int, int, int, int,
                                            TextRows *, const char *, int);
static void nv_ncurses_pager_update(DataStruct *, PagerStruct *);
static void nv_ncurses_pager_handle_events(DataStruct *, PagerStruct *, int);
static void nv_ncurses_destroy_pager(PagerStruct *);
static int nv_ncurses_paged_prompt(Options *, const char*, const char*,
                                   const char*, const char * const *, int, int);


/* progress bar helper functions */

static int choose_char(int i, int p[4], char v[4], char def);
static void init_percentage_string(char v[4], int n);
static void init_position(int p[4], int w);


/* misc helper functions */

static char *nv_ncurses_create_command_list_text(DataStruct *, CommandList *);
static char *nv_ncurses_mode_to_permission_string(mode_t);

static void nv_ncurses_free_text_rows(TextRows *);

static int nv_ncurses_format_print(DataStruct *, RegionStruct *,
                                   int, int, int, int, TextRows *t);

static int nv_ncurses_check_resize(DataStruct *, bool);







/* dispatch table that gets dlsym()'ed by ui_init() */

InstallerUI ui_dispatch_table = {
    nv_ncurses_detect,
    nv_ncurses_init,
    nv_ncurses_set_title,
    nv_ncurses_get_input,
    nv_ncurses_message,
    nv_ncurses_command_output,
    nv_ncurses_approve_command_list,
    nv_ncurses_yes_no,
    nv_ncurses_multiple_choice,
    nv_ncurses_paged_prompt,
    nv_ncurses_status_begin,
    nv_ncurses_status_update,
    nv_ncurses_status_end,
    nv_ncurses_close
};




/* internal variables */

/* Save the return value of initscr() here instead of using stdscr directly.
 * This avoids problems if libncurses.so doesn't expose stdscr, as was the
 * case in http://bugzilla.suse.com/show_bug.cgi?id=1132282 */
static WINDOW *nv_stdscr;




/*
 * nv_ncurses_detect() - initialize ncurses; return FALSE if
 * initialization fails
 */

static int nv_ncurses_detect(Options *op)
{
    int x, y;

    if (!(nv_stdscr = initscr())) return FALSE;

    /*
     * query the current size of the window, and don't try to use the
     * ncurses ui if it's too small.
     */

    getmaxyx(nv_stdscr, y, x);

    if ((x < NV_NCURSES_MIN_WIDTH) || (y < NV_NCURSES_MIN_HEIGHT)) {
        endwin();
        return FALSE;
    }

    return TRUE;

} /* nv_ncurses_detect() */




/*
 * nv_ncurses_init() - initialize the ncurses interface.
 */

static int nv_ncurses_init(Options *op, FormatTextRows format_text_rows)
{
    DataStruct *d;
    
    d = (DataStruct *) malloc(sizeof(DataStruct));
    memset(d, 0, sizeof(DataStruct));
    
    d->format_text_rows = format_text_rows;
    
    /* initialize color */
    
    d->use_color = !op->no_ncurses_color;

    if (d->use_color) {
        if (!has_colors()) {
            d->use_color = FALSE;
        }
    }
    
    if (d->use_color) {
        if (start_color() == ERR) {
            d->use_color = FALSE;
        } else {
            /*                                      foreground    background */
            init_pair(NV_NCURSES_HEADER_COLOR_IDX,  COLOR_BLACK,  COLOR_GREEN);
            init_pair(NV_NCURSES_MESSAGE_COLOR_IDX, COLOR_WHITE,  COLOR_BLUE);
            init_pair(NV_NCURSES_BUTTON_COLOR_IDX,  COLOR_WHITE,  COLOR_RED);
            init_pair(NV_NCURSES_INPUT_COLOR_IDX,   COLOR_GREEN,  COLOR_BLACK);
        }
    }
    
    wclear(nv_stdscr);       /* clear the screen */
    noecho();                /* don't echo input to the screen */
    cbreak();                /* disable line buffering and control characters */
    curs_set(0);             /* make the cursor invisible */
    keypad(nv_stdscr, TRUE); /* enable keypad, function keys, arrow keys, etc */
    
    getmaxyx(nv_stdscr, d->height, d->width); /* get current window dimensions */
    
    /* create the regions */
    
    d->header = nv_ncurses_create_region(d, 1, 0, d->width - 2, 1,
                                         NV_NCURSES_HEADER_COLOR,
                                         NV_NCURSES_HEADER_NO_COLOR);

    d->footer = nv_ncurses_create_region(d, 1, d->height - 2, d->width - 2, 1,
                                         NV_NCURSES_FOOTER_COLOR,
                                         NV_NCURSES_FOOTER_NO_COLOR);
    
    /* plug the DataStruct struct into the ui_priv pointer */
    
    op->ui_priv = (void *) d;
    
    /* set the initial strings in the header and footer */
    
    nv_ncurses_set_header(d, NV_NCURSES_DEFAULT_TITLE);
    
    nv_ncurses_set_footer(d, NV_NCURSES_DEFAULT_FOOTER_LEFT,
                          NV_NCURSES_DEFAULT_FOOTER_RIGHT);
    
    wrefresh(nv_stdscr);
    
    return TRUE;
    
} /* nv_ncurses_init() */




/*
 * nv_ncurses_set_title() - update the string in the header region
 */

static void nv_ncurses_set_title(Options *op, const char *title)
{
    DataStruct *d = (DataStruct *) op->ui_priv;

    nv_ncurses_set_header(d, title);

    wrefresh(nv_stdscr);

} /* nv_ncurses_set_title() */




/*
 * nv_ncurses_get_input - prompt for user input with the given msg;
 * returns the user inputed string.
 */

#define MIN_INPUT_LEN 32
#define MAX_BUF_LEN 1024
#define BUF_CHAR(c) ((c) ? (c) : ' ')

static char *nv_ncurses_get_input(Options *op,
                                  const char *def, const char *msg)
{
    DataStruct *d = (DataStruct *) op->ui_priv;
    int msg_len, width, input_len, buf_len, def_len;
    int input_x, input_y, i, w, h, x, y, c, lines, ch, color, redraw;
    char *tmp, buf[MAX_BUF_LEN];
    TextRows *t;

    if (!msg) return NULL;

    nv_ncurses_check_resize(d, FALSE);

    color = d->use_color ? NV_NCURSES_INPUT_COLOR : A_REVERSE;

    /* concatenate ": " to the end of the message */

    msg_len = strlen(msg) + 2;
    tmp = (char *) malloc(msg_len + 1);
    snprintf(tmp, msg_len, "%s: ", msg);

    /* copy the default response into the buffer */

    memset(buf, 0, MAX_BUF_LEN);
    if (def) strncpy(buf, def, MAX_BUF_LEN);
    
 draw_get_input:
    
    /* free any existing message region */

    if (d->message) {
        nv_ncurses_destroy_region(d->message);
        d->message = NULL;
    }

    /*
     * compute the size of the input box: the input box is twice the
     * length of the default string, clamped to MIN_INPUT_LEN
     */

    def_len = def ? strlen(def) : 0;
    input_len = NV_MAX(def_len * 2, MIN_INPUT_LEN);
    width = d->width - 4;

    /* convert the message to text rows */

    t = d->format_text_rows(NULL, tmp, width, TRUE);
    
    /*
     * if the message and input box will fit on one line, do that;
     * otherwise, place them on separate lines
     */

    if ((msg_len + input_len + 1) < width) {
        input_x = 1 + msg_len;
        lines = 1;
    } else {
        input_x = 2;
        lines = t->n + 1;
    }
    
    input_y = lines;

    /*
     * compute the width, height, and starting position of the message
     * region
     */

    w = d->width - 2;
    h = lines + 2;
    x = 1;
    y = ((d->height - (3 + h)) / 3) + 1;

    /* create the message region */

    d->message = nv_ncurses_create_region(d, x, y, w, h,
                                          NV_NCURSES_MESSAGE_COLOR,
                                          NV_NCURSES_MESSAGE_NO_COLOR);

    nv_ncurses_format_print(d, d->message, 1, 1, d->message->w - 2, lines, t);

    /* free the text rows */

    nv_ncurses_free_text_rows(t);

    /* clamp the input box to the width of the region */
    
    input_len = NV_MIN(input_len, width - 2);

    curs_set(1); /* make the cursor visible */

    c = buf_len = strlen(buf);
    
    redraw = TRUE; /* force a redraw the first time through */

    /* offset input_x and input_y by the region offset */

    input_x += d->message->x;
    input_y += d->message->y;

    do {
        x = NV_MAX(c - (input_len - 1), 0);

        /* redraw the input box */
        
        if (redraw) {
            for (i = 0; i < input_len; i++) {
                mvwaddch(nv_stdscr, input_y, input_x + i,
                         BUF_CHAR(buf[i + x]) | color);
            }
        
            /* if we're scrolling, display an arrow */

            if (x > 0) {
                mvwaddch(nv_stdscr, input_y, input_x - 1,
                         '<' | d->message->attr);
            } else {
                mvwaddch(nv_stdscr, input_y, input_x - 1,
                         ' ' | d->message->attr);
            }
        
            if (buf_len > (input_len - 1 + x)) {
                mvwaddch(nv_stdscr, input_y, input_x + input_len,
                         '>' | d->message->attr);
            } else {
                mvwaddch(nv_stdscr, input_y, input_x + input_len,
                         ' ' | d->message->attr);
            }
            
            redraw = FALSE;
        }

        /* position the cursor */

        wmove(nv_stdscr, input_y, input_x - x + c);
        wrefresh(nv_stdscr);
        
        /* wait for input */

        if (nv_ncurses_check_resize(d, FALSE)) goto draw_get_input;
        ch = wgetch(nv_stdscr);

        switch (ch) {
            
        case NV_NCURSES_BACKSPACE:
        case KEY_BACKSPACE:

            /*
             * If there is a character to be deleted, move everything
             * after the character forward, and decrement c and len.
             */
            
            if (c <= 0) break;
            for (i = c; i <= buf_len; i++) buf[i-1] = buf[i];
            c--;
            buf_len--;
            redraw = TRUE;
            break;
            
        case KEY_DC:

            /*
             * If there is a character to be deleted, move everything
             * after the character forward, and decrement len.
             */

            if (c == buf_len) break;
            for (i = c; i < buf_len; i++) buf[i] = buf[i+1];
            buf_len--;
            redraw = TRUE;
            break;

        case KEY_LEFT:
            if (c > 0) {
                c--;
                redraw = TRUE;
            }
            break;

        case KEY_RIGHT:
            if (c < buf_len) {
                c++;
                redraw = TRUE;
            }
            break;

        case NV_NCURSES_CTRL('L'):
            nv_ncurses_check_resize(d, TRUE);
            goto draw_get_input;
            break;
        }
        
        /*
         * If we have a printable character, then move everything
         * after the current location back, and insert the character.
         */

        if (isprint(ch)) {
            if (buf_len < (MAX_BUF_LEN - 1)) {
                for (i = buf_len; i > c; i--) buf[i] = buf[i-1];
                buf[c] = (char) ch;
                buf_len++;
                c++;
                redraw = TRUE;
            }
        }

        if (op->debug) {
            mvprintw(d->message->y, d->message->x,
                     "c: %3d  ch: %04o (%d)", c, ch, ch);
            wclrtoeol(nv_stdscr);
            redraw = TRUE;
        }
        
    } while (ch != NV_NCURSES_ENTER);

    /* free the message region */
    
    nv_ncurses_destroy_region(d->message);
    d->message = NULL;
    
    curs_set(0); /* make the cursor invisible */
    wrefresh(nv_stdscr);
    
    free(tmp);
    tmp = strdup(buf);
    return tmp;

} /* nv_ncurses_get_input() */



/*
 * nv_ncurses_message() - print a message
 */

static void nv_ncurses_message(Options *op, int level, const char *msg)
{
    DataStruct *d = (DataStruct *) op->ui_priv;
    int w, h, x, y, ch;
    char *prefix;
   
    if (!msg) return;

    /* XXX for now, log messages are ignored by the ncurses ui */

    if (level == NV_MSG_LEVEL_LOG) return;
    
    /* determine the prefix for the message from the message level */

    switch(level) {
    case NV_MSG_LEVEL_MESSAGE:
        prefix = NULL;
        break;
    case NV_MSG_LEVEL_WARNING:
        prefix = "WARNING: ";
        break;
    case NV_MSG_LEVEL_ERROR:
        prefix = "ERROR: ";
        break;
    default:
        return;
    }

 draw_message:

    if (d->message) {
        
        /*
         * XXX we may already have a message region allocated when we
         * enter nv_ncurses_message(): we may have been in the middle
         * of displaying a progress bar when we encountered an error
         * that we need to report.  To deal with this situation, throw
         * out the existing message region; nv_ncurses_status_update()
         * and nv_ncurses_status_end() will have to recreate their
         * message region.
         */
        
        nv_ncurses_destroy_region(d->message);
        d->message = NULL;
    }

    /* create the message region and print msg in it */

    nv_ncurses_do_message_region(d, prefix, msg, FALSE, 2);
    
    /* init the dimensions of the button */

    w = 6;
    h = 1;
    x = (d->message->w - w) / 2;
    y = d->message->h - 2;

    /* draw the OK button */

    nv_ncurses_draw_button(d, d->message, x, y, w, h, "OK",
                           TRUE, FALSE);
    wrefresh(nv_stdscr);
    
    /* wait for enter */

    do {
        /* if a resize occurred, jump back to the top and redraw */
        
        if (nv_ncurses_check_resize(d, FALSE)) goto draw_message;
        ch = wgetch(nv_stdscr);

        switch (ch) {
        case NV_NCURSES_CTRL('L'):
            nv_ncurses_check_resize(d, TRUE);
            goto draw_message;
            break;
        }
    } while (ch != NV_NCURSES_ENTER);
    
    /* animate the button being pushed down */

    nv_ncurses_erase_button(d->message, x, y, w, h);
    nv_ncurses_draw_button(d, d->message, x, y, w, h, "OK", TRUE, TRUE);
    wrefresh(nv_stdscr);
    usleep(NV_NCURSES_BUTTON_PRESS_TIME);

    nv_ncurses_erase_button(d->message, x, y, w, h);
    nv_ncurses_draw_button(d, d->message, x, y, w, h, "OK", TRUE, FALSE);
    wrefresh(nv_stdscr);
    usleep(NV_NCURSES_BUTTON_PRESS_TIME);

    /* free the message region */

    nv_ncurses_destroy_region(d->message);
    d->message = NULL;
    wrefresh(nv_stdscr);

} /* nv_ncurses_message() */




/* 
 * nv_ncurses_command_output() - drop this on the floor, for now.
 */

static void nv_ncurses_command_output(Options *op, const char *msg)
{
    /* 
     * XXX we don't currently display command output in the ncurses ui
     */

    return;

} /* nv_ncurses_command_output() */




/*
 * nv_ncurses_approve_command_list() - list all the commands that will be
 * executed, and ask for approval.
 */

static int nv_ncurses_approve_command_list(Options *op, CommandList *cl,
                                           const char *descr)
{
    DataStruct *d = (DataStruct *) op->ui_priv;
    char *commandlist, *question;
    int ret, len;
    const char *buttons[2] = {"Yes", "No"};

    /* initialize the question string */

    len = strlen(descr) + 256;
    question = (char *) malloc(len + 1);
    snprintf(question, len, "The following operations will be performed to "
             "install the %s.  Is this acceptable?", descr);
    
    commandlist = nv_ncurses_create_command_list_text(d, cl);

    ret = nv_ncurses_paged_prompt(op, question, "Proposed CommandList",
                                  commandlist, buttons, 2, 0);

    free(question);
    free(commandlist);

    return ret == 0;
} /* nv_ncurses_approve_command_list() */




/*
 * nv_ncurses_yes_no() - ask the yes/no question 'msg' and return TRUE for
 * yes, and FALSE for no.
 */

static int nv_ncurses_yes_no(Options *op, const int def, const char *msg)
{
    const char *buttons[2] = { "Yes", "No" };

    return (nv_ncurses_multiple_choice(op, msg, buttons, 2,
                                       (def) ? 0 : 1) == 0);
} /* nv_ncurses_yes_no() */


/*
 * nv_ncurses_multiple_choice() - display a question with multiple-choice
 * buttons allowing the user to select a button corresponding to the desired
 * response. Returns the index of the button selected from the passed-in
 * buttons array.
 */

static int nv_ncurses_multiple_choice(Options *op, const char *question,
                                      const char * const *buttons, int num_buttons,
                                      int default_button)
{
    return nv_ncurses_paged_prompt(op, question, NULL, NULL, buttons,
                                   num_buttons, default_button);
} /* nv_ncurses_multiple_choice() */




/*
 * nv_ncurses_status_begin() - initialize a progress bar
 */

static void nv_ncurses_status_begin(Options *op,
                                    const char *title, const char *msg)
{
    DataStruct *d = (DataStruct *) op->ui_priv;
   
    nv_ncurses_check_resize(d, FALSE);

    /* cache the progress bar title */

    d->progress_title = strdup(title);

    /* create the message region for use by the progress bar */

    nv_ncurses_do_progress_bar_region(d);

    /* write the one line msg (truncating it, if need be) */
    
    nv_ncurses_do_progress_bar_message(d, msg, d->message->h - 3,
                                       d->message->w);

    wrefresh(nv_stdscr);

} /* nv_ncurses_status_begin() */




/*
 * nv_ncurses_status_update() - update the progress bar
 */

static void nv_ncurses_status_update(Options *op, const float percent,
                                     const char *msg)
{
    int i, n, h, ch;
    int p[4];
    char v[4];
    DataStruct *d = (DataStruct *) op->ui_priv;

    /* 
     * if the message region was deleted or if the window was resized,
     * redraw the entire progress bar region.
     */

    if (nv_ncurses_check_resize(d, FALSE) || !d->message) {
        if (d->message) nv_ncurses_destroy_region(d->message);
        nv_ncurses_do_progress_bar_region(d);
    }

    /* temporarily set getch() to non-blocking mode */

    nodelay(nv_stdscr, TRUE);

    while ((ch = wgetch(nv_stdscr)) != ERR) {
        /*
         * if the user explicitely requested that the screen be
         * redrawn by pressing CTRL-L, then also redraw the entire
         * progress bar egion.
         */
        if (ch == NV_NCURSES_CTRL('L')) {
            nv_ncurses_check_resize(d, TRUE);
            nv_ncurses_destroy_region(d->message);
            nv_ncurses_do_progress_bar_region(d);
        }
    }

    /* set getch() back to blocking mode */

    nodelay(nv_stdscr, FALSE);

    /* compute the percentage */

    n = ((int) (percent * (float) (d->message->w - 2)));
    n = NV_MAX(n, 2);
    
    init_position(p, d->message->w);
    init_percentage_string(v, (int) (100.0 * percent));
    
    h = d->message->h;
    
    /* write the one line msg (truncating it, if need be) */

    nv_ncurses_do_progress_bar_message(d, msg, h - 3, d->message->w - 2);

    /* draw the progress bar */

    if (d->use_color) {
        for (i = 1; i <= n; i++) {
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v,' ') |
                    A_REVERSE | NV_NCURSES_INPUT_COLOR);
        }

        for (i = 0; i < 4; i++) {
            if (p[i] >= (n+1)) {
                mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + p[i],
                        (v[i] ? v[i] : ' ') | NV_NCURSES_INPUT_COLOR);
            }
        }
    } else {
        for (i = 2; i < n; i++) {
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v, ' ') | A_REVERSE);
        }
        
        for (i = 0; i < 4; i++) {
            if (p[i] >= n) {
                mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + p[i],
                        v[i] ? v[i] : '-');
            }
        }
    }

    wrefresh(nv_stdscr);
    
} /* nv_ncurses_status_update() */




/*
 * nv_ncurses_status_end() - draw the progress bar at 100%
 */

static void nv_ncurses_status_end(Options *op, const char *msg)
{
    int i, n, h;
    int p[4];
    char v[4];
    DataStruct *d = (DataStruct *) op->ui_priv;  

    /* 
     * if the message region was deleted or if the window was resized,
     * redraw the entire progress bar region.
     */

    if (nv_ncurses_check_resize(d, FALSE) || !d->message) {
        if (d->message) nv_ncurses_destroy_region(d->message);
        nv_ncurses_do_progress_bar_region(d);
    }
    
    n = d->message->w - 2;
    
    init_position(p, d->message->w);
    init_percentage_string(v, 100.0);
    
    h = d->message->h;
    
    /* write the one line msg (truncating it, if need be) */

    nv_ncurses_do_progress_bar_message(d, msg, h - 3, d->message->w - 2);

    /* draw the complete progress bar */

    if (d->use_color) {
        for (i = 1; i < (n+1); i++) {
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v, ' ') |
                    A_REVERSE | NV_NCURSES_INPUT_COLOR);
        }
    } else {
        for (i = 2; i < (n); i++) {
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v, ' ') | A_REVERSE);
        }
    }
    
    wrefresh(nv_stdscr);

    free(d->progress_title);
    d->progress_title = NULL;
    
    /* XXX don't free the message window, yet... */

} /* nv_ncurses_status_end() */




/*
 * nv_ncurses_close() - close the ui: free any memory we allocated,
 * and end curses mode.
 */

static void nv_ncurses_close(Options *op)
{
    DataStruct *d = NULL;

    if (op) {

        /* XXX op may be NULL if we get called from a signal handler */

        d = (DataStruct *) op->ui_priv;  
        nv_ncurses_destroy_region(d->header);
        nv_ncurses_destroy_region(d->footer);
        free(d);
    }
    
    wclear(nv_stdscr);
    wrefresh(nv_stdscr);
    
    endwin();  /* End curses mode */
    
    return;
    
} /* nv_ncurses_close() */



/****************************************************************************/
/*
 * internal helper functions for manipulating the header and footer
 */




/*
 * nv_ncurses_set_header() - write the title to the header region;
 * note that this function does not call refresh()
 */

static void nv_ncurses_set_header(DataStruct *d, const char *title)
{
    int x, y;
    char *tmp;

    tmp = strdup(title);

    if (d->title) free(d->title);
    d->title = tmp;
    
    x = (d->header->w - strlen(d->title)) / 2;
    y = 0;

    wattrset(nv_stdscr, d->header->attr);
    
    nv_ncurses_clear_region(d->header);
    mvwaddstr(nv_stdscr, d->header->y + y, d->header->x + x, (char *) d->title);
    wattrset(nv_stdscr, A_NORMAL);
    
} /* nv_ncurses_set_header() */




/*
 * nv_ncurses_set_footer() - write the left and right text to the
 * footer; note that this function does not call refresh()
 */

static void nv_ncurses_set_footer(DataStruct *d, const char *left,
                                  const char *right)
{
    int x, y;
    char *tmp0, *tmp1;

    tmp0 = strdup(left);
    tmp1 = strdup(right);

    if (d->footer_left) free(d->footer_left);
    if (d->footer_right) free(d->footer_right);
    
    d->footer_left = tmp0;
    d->footer_right = tmp1;

    wattrset(nv_stdscr, d->footer->attr);
    
    nv_ncurses_clear_region(d->footer);
    
    if (d->footer_left) {
        y = 0;
        x = 1;
        mvwaddstr(nv_stdscr, d->footer->y + y, d->footer->x + x,
                  d->footer_left);
    }
    if (d->footer_right) {
        y = 0;
        x = d->footer->w - strlen(d->footer_right) - 1;
        mvwaddstr(nv_stdscr, d->footer->y + y, d->footer->x + x,
                  d->footer_right);
    }

    wattrset(nv_stdscr, A_NORMAL);
    
} /* nv_ncurses_set_footer() */




/****************************************************************************/
/*
 * internal helper functions for manipulating RegionStructs
 */




/*
 * nv_ncurses_create_region() - create a new region at the specified
 * location with the specified dimensions; note that this function
 * does not call refresh()
 */

static RegionStruct *nv_ncurses_create_region(DataStruct *d,
                                              int x, int y, int w, int h,
                                              int color,
                                              int no_color_attr)
{
    RegionStruct *region =
        (RegionStruct *) malloc(sizeof(RegionStruct));
    
    region->x = x;
    region->y = y;
    region->w = w;
    region->h = h;

    if (d->use_color) region->attr = color;
    else              region->attr = no_color_attr;
    
    /* create a single line for use in clearing the region */

    region->line = (char *) malloc(w + 1);
    memset(region->line, ' ', w);
    region->line[w] = '\0';

    /* clear the region */

    wattrset(nv_stdscr, region->attr);
    nv_ncurses_clear_region(region);
    wattrset(nv_stdscr, A_NORMAL);
    
    return region;

} /* nv_ncurses_create_region() */




/*
 * nv_ncurses_clear_region() - clear each line in the region; note
 * that this function does not call refresh(), nor does it explicitly
 * set any attributes.
 */

static void nv_ncurses_clear_region(RegionStruct *region)
{
    int i;

    for (i = region->y; i < (region->y + region->h); i++) {
        mvwaddstr(nv_stdscr, i, region->x, region->line);
    }
} /* nv_ncurses_clear_region() */




/*
 * nv_ncurses_destroy_region() - clear and free the RegionStruct; note
 * that this function does not call refresh()
 */

static void nv_ncurses_destroy_region(RegionStruct *region)
{
    if (!region) return;

    wattrset(nv_stdscr, A_NORMAL);
    nv_ncurses_clear_region(region);
    free(region->line);
    free(region);
    
} /* nv_ncurses_destroy_region() */
    

                                  

/****************************************************************************/
/*
 * internal helper functions for drawing buttons
 */




/*
 * nv_ncurses_draw_button() - draw a button on the specified region,
 * at location x,y with dimensions w,h.  Give the button the label
 * str.
 *
 * The hilite parameter, when TRUE, causes the label to be printed in
 * reverse colors.
 *
 * The down parameter, when TRUE, causes the button to be drawn as if
 * it had been pressed down (ie: shifted down and right).
 */

static void nv_ncurses_draw_button(DataStruct *d, RegionStruct *region,
                                   int x, int y, int w, int h,
                                   const char *str, bool hilite, bool down)
{
    int i, j, n, attr = 0;
    
    if (down) x++, y++;
    
    n = strlen(str);
    n = (n > w) ? 0 : ((w - n) / 2);

    if (d->use_color) {
        attr = NV_NCURSES_BUTTON_COLOR;
    } else if (hilite) {
        attr = A_REVERSE;
    } else {
        attr = 0;
    }
        
    for (j = y; j < (y + h); j++) {
        for (i = x; i < (x + w); i++) {
            mvwaddch(nv_stdscr, region->y + j, region->x + i, ' ' | attr);
        }
    }
    
    if (hilite) attr |= A_REVERSE;
    
    wattron(nv_stdscr, attr);
    mvwaddstr(nv_stdscr, region->y + y + h/2, region->x + x + n, str);
    wattroff(nv_stdscr, attr);

} /* nv_ncurses_draw_button() */




/*
 * nv_ncurses_erase_button() - erase the button on the specified
 * region at location x,y with dimensions w,h.
 */

static void nv_ncurses_erase_button(RegionStruct *region,
                                    int x, int y, int w, int h)
{
    int i, j;
    
    for (j = y; j <= (y + h); j++) {
        for (i = x; i <= (x + w); i++) {
            mvwaddch(nv_stdscr, region->y + j, region->x + i,
                     ' ' | region->attr);
        }
    }
    
} /* nv_ncurses_erase_button() */




/*****************************************************************************/
/*
 * nv_ncurses_do_message_region(),
 * nv_ncurses_do_progress_bar_region(),
 * nv_ncurses_do_progress_bar_message() - helper functions for drawing
 * regions and stuff.
 */

/*
 * nv_ncurses_do_message_region() - create a new message region
 * containing the string msg.  The "top" argument indicates whether
 * the region should be vertically positioned immediately below the
 * header, or should be positioned 1/3 of the way down the screen.
 * The num_extra_lines argument is used to request extra lines in the
 * message region below the string (to leave room for buttons, for
 * example).
 */

static void nv_ncurses_do_message_region(DataStruct *d, const char *prefix,
                                         const char *msg, int top,
                                         int num_extra_lines)
{
    int w, h, x, y;
    TextRows *t;

    /*
     * compute the width and height that we need (taking into account
     * num_extra_lines that the caller may need for buttons
     */

    w = d->width - 2;
    t = d->format_text_rows(prefix, msg, w - 2, TRUE);
    h = t->n + num_extra_lines + 2;
    
    /*
     * compute the starting position of the message region: either
     * immediately below the header or 1/3 of the way down the screen.
     */

    x = 1;
    if (top) y = 2;
    else     y = ((d->height - (3 + h)) / 3) + 1;
    
    /* create the message region */
    
    d->message = nv_ncurses_create_region(d, x, y, w, h,
                                          NV_NCURSES_MESSAGE_COLOR,
                                          NV_NCURSES_MESSAGE_NO_COLOR);
    
    nv_ncurses_format_print(d, d->message, 1, 1, d->message->w - 2,
                            d->message->h - (1 + num_extra_lines), t);

    /* free the text rows */

    nv_ncurses_free_text_rows(t);
    
} /* nv_ncurses_do_message_region() */




/*
 * nv_ncurses_do_progress_bar_region() - create a message region, draw
 * the progress bar's title, separator, and the empty progress bar.
 */

static void nv_ncurses_do_progress_bar_region(DataStruct *d)
{
    int n, h, i, p[4];
    char v[4];

    /* create the message region and print the title in it */
    
    nv_ncurses_do_message_region(d, NULL, d->progress_title, FALSE, 3);

    n = d->message->w - 2;
    h = d->message->h;
    
    wattrset(nv_stdscr, d->message->attr);

    /* draw the horizontal separator */

    for (i = 1; i <= n; i++) {
        mvwaddch(nv_stdscr, d->message->y + h - 4, d->message->x + i,
                NV_NCURSES_HLINE | d->message->attr);
    }

    /* draw an empty progress bar */

    init_position(p, n + 2);
    init_percentage_string(v, 0);
    v[2] = '0';

    if (d->use_color) {
        for (i = 1; i <= n; i++) {
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v, ' ') | NV_NCURSES_INPUT_COLOR);
        }
    } else {
        mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + 1, '[');
        for (i = 2; i < n; i++)
            mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + i,
                    choose_char(i, p, v, '-'));
        mvwaddch(nv_stdscr, d->message->y + h - 2, d->message->x + n, ']');
    }

} /* nv_ncurses_do_progress_bar_region() */



/*
 * nv_ncurses_do_progress_bar_message() - write the one line progress
 * bar message to the message region, truncating the string, if need
 * be.
 */

static void nv_ncurses_do_progress_bar_message(DataStruct *d, const char *str,
                                               int y, int w)
{
    char *tmp;

    /* clear the message line */

    wattrset(nv_stdscr, d->message->attr);
    mvwaddstr(nv_stdscr, d->message->y + y, d->message->x, d->message->line);

    /* write the message string */
    
    if (str) {
        tmp = malloc(w + 1);
        strncpy(tmp, str, w);
        tmp[w] = '\0';
        mvwaddstr(nv_stdscr, d->message->y + y, d->message->x + 1, tmp);
        free(tmp);
    }
    
} /* nv_ncurses_do_progress_bar_message() */




/***************************************************************************/
/*
 * pager functions
 */


/*
 * pager functions -- these functions provide the basic behavior of a
 * text viewer... used to display TextRows.
 *
 *  d      : DataStruct struct
 *  x      : starting x coordinate of the pager
 *  y      : starting y coordinate of the pager
 *  w      : width of the pager
 *  h      : height of the pager
 *  t      : TextRows to be displayed
 *  label  : string to be displayed in the status bar 
 *  cur    : initial current line of the pager
 */

static PagerStruct *nv_ncurses_create_pager(DataStruct *d,
                                            int x, int y, int w, int h,
                                            TextRows *t, const char *label,
                                            int cur)
{
    PagerStruct *p = (PagerStruct *) malloc(sizeof(PagerStruct));
    
    p->t = t;
    p->region = nv_ncurses_create_region(d, x, y, w, h, A_NORMAL, A_NORMAL);
    p->label = label;
    
    p->cur = cur;
    
    p->page = h - 2;

    nv_ncurses_pager_update(d, p);

    return p;

} /* nv_ncurses_create_pager() */




/*
 * nv_ncurses_destroy_pager() - free resources associated with the
 * pager
 */

static void nv_ncurses_destroy_pager(PagerStruct *p)
{
    nv_ncurses_destroy_region(p->region);
    free(p);
    
} /* nv_ncurses_destroy_pager () */



/*
 * nv_ncurses_pager_update() - redraw the text in the pager, and
 * update the information about the pager in the footer.  Note that
 * this function does not call refresh().
 */

static void nv_ncurses_pager_update(DataStruct *d, PagerStruct *p)
{
    int i, maxy, percent, denom;
    char tmp[10];

    if (!p) return;

    /* determine the maximum y value for the text */

    maxy = (p->cur + (p->region->h - 1));
    if (maxy > p->t->n) maxy = p->t->n;

    /* draw the text */

    wattrset(nv_stdscr, p->region->attr);

    for (i = p->cur; i < maxy; i++) {
        mvwaddstr(nv_stdscr, p->region->y + i - p->cur, p->region->x,
                  p->region->line);
        if (p->t->t[i]) {
            mvwaddstr(nv_stdscr, p->region->y + i - p->cur, p->region->x,
                      p->t->t[i]);
        }
    }

    /* compute the percentage */
    
    denom = p->t->n - (p->region->h - 1);
    if (denom < 1) percent = 100;
    else percent = ((100.0 * (float) (p->cur)) / (float) denom);
    
    /* create the percentage string */
    
    if (p->t->n <= (p->region->h - 1)) snprintf(tmp, 10, "All");
    else if (percent <= 0)          snprintf(tmp, 10, "Top");
    else if (percent >= 100)        snprintf(tmp, 10, "Bot");
    else                            snprintf(tmp, 10, "%3d%%", percent);

    /* update the status in the footer */

    nv_ncurses_set_footer(d, p->label, tmp);
    
} /* nv_ncurses_pager_update() */



/*
 * nv_ncurses_pager_handle_events() - process any keys that affect the
 * pager.
 */

static void nv_ncurses_pager_handle_events(DataStruct *d,
                                           PagerStruct *p, int ch)
{
    int n;
    
    if (!p) return;
    n = p->t->n - (p->region->h - 1);

    switch (ch) {
    case KEY_UP:
        if (p->cur > 0) {
            p->cur--;
            nv_ncurses_pager_update(d, p);
            wrefresh(nv_stdscr);
        }
        break;

    case KEY_DOWN:
        if (p->cur < n) {
            p->cur++;
            nv_ncurses_pager_update(d, p);
            wrefresh(nv_stdscr);
        }
        break;

    case KEY_PPAGE:
        if (p->cur > 0) {
            p->cur -= p->page;
            if (p->cur < 0) p->cur = 0;
            nv_ncurses_pager_update(d, p);
            wrefresh(nv_stdscr);
        }
        break;

    case KEY_NPAGE:
        if (p->cur < n) {
            p->cur += p->page;
            if (p->cur > n) p->cur = n;
            nv_ncurses_pager_update(d, p);
            wrefresh(nv_stdscr);
        }
        break;
    }
} /* nv_ncurses_pager_handle_events() */

static void draw_buttons(DataStruct *d, const char * const *buttons,
                         int num_buttons, int button, int button_w,
                         const int *buttons_x, int button_y)
{
    int i;

    for (i = 0; i < num_buttons; i++) {
        nv_ncurses_draw_button(d, d->message, buttons_x[i], button_y,
                               button_w, 1, buttons[i], button == i, FALSE);
    }
}

/*
 * nv_ncurses_paged_prompt() - display a question with multiple-choice buttons
 * along with a scrollable paged text area, allowing the user to review the
 * pageable text and select a button corresponding to the desired response.
 * Returns the index of the button selected from the passed-in buttons array.
 */

static int nv_ncurses_paged_prompt(Options *op, const char *question,
                                   const char *pager_title,
                                   const char *pager_text,
                                   const char * const *buttons,
                                   int num_buttons, int default_button)
{
    DataStruct *d = (DataStruct *) op->ui_priv;
    TextRows *t_pager = NULL;
    int ch, cur = 0;
    int i, button_w = 0, button_y, button = default_button;
    int buttons_x[num_buttons];
    PagerStruct *p = NULL;

    /* check if the window was resized */

    nv_ncurses_check_resize(d, FALSE);

    for (i = 0; i < num_buttons; i++) {
        int len = strlen(buttons[i]);
        if (len > button_w) {
            button_w = len;
        }
    }
    button_w += 4;

  print_message:

    /* free any existing message region, pager, and pager textrows */

    if (d->message) {

        /*
         * XXX we may already have a message region allocated when we
         * enter nv_ncurses_paged_prompt(): we may have been in the middle
         * of displaying a progress bar when we encountered an error
         * that we need to report.  To deal with this situation, throw
         * out the existing message region; nv_ncurses_status_update()
         * and nv_ncurses_status_end() will have to recreate their
         * message region.
         */

        nv_ncurses_destroy_region(d->message);
        d->message = NULL;
    }

    if (p) {
        nv_ncurses_destroy_pager(p);
        p = NULL;
    }
    if (t_pager) {
        nv_ncurses_free_text_rows(t_pager);
        t_pager = NULL;
    }

    /* create the message region and print the question in it */

    nv_ncurses_do_message_region(d, NULL, question,
                                 (pager_title && pager_text), 2);

    button_y = d->message->h - 2;

    for (i = 0; i < num_buttons; i++) {
        buttons_x[i] = (i + 1) * (d->message->w / (num_buttons + 1)) -
                       button_w / 2;
    }

    draw_buttons(d, buttons, num_buttons, button, button_w, buttons_x,
                 button_y);

    /* draw the paged text */

    if (pager_title && pager_text) {
        t_pager = d->format_text_rows(NULL, pager_text, d->message->w, TRUE);
        p = nv_ncurses_create_pager(d, 1, d->message->h + 2, d->message->w,
                                    d->height - d->message->h - 4, t_pager,
                                    pager_title, cur);
    }

    wrefresh(nv_stdscr);

    /* process key strokes */

    do {
        /* if a resize occurred, jump back to the top and redraw */
            if (nv_ncurses_check_resize(d, FALSE)) {
                if (p) {
                    cur = p->cur;
                }
                goto print_message;
        }

        ch = wgetch(nv_stdscr);

        switch (ch) {
            case NV_NCURSES_TAB:
            case KEY_RIGHT:
                button = (button + 1) % num_buttons;
                draw_buttons(d, buttons, num_buttons, button, button_w,
                             buttons_x, button_y);
                wrefresh(nv_stdscr);
                break;

            case KEY_LEFT:
                button = (button + num_buttons - 1) % num_buttons;
                draw_buttons(d, buttons, num_buttons, button, button_w,
                             buttons_x, button_y);
                wrefresh(nv_stdscr);
                break;

            case NV_NCURSES_CTRL('L'):
                nv_ncurses_check_resize(d, TRUE);
                if (p) {
                    cur = p->cur;
                }
                goto print_message;
                break;

            default:
                break;
        }

        if (p) {
            nv_ncurses_pager_handle_events(d, p, ch);
        }
    } while (ch != NV_NCURSES_ENTER);

    /* animate the button being pushed down */

    nv_ncurses_erase_button(d->message, buttons_x[button], button_y,
                            button_w, 1);
    nv_ncurses_draw_button(d, d->message, buttons_x[button], button_y,
                           button_w, 1, buttons[button], TRUE, TRUE);
    wrefresh(nv_stdscr);
    usleep(NV_NCURSES_BUTTON_PRESS_TIME);

    nv_ncurses_erase_button(d->message, buttons_x[button], button_y,
                            button_w, 1);
    nv_ncurses_draw_button(d, d->message, buttons_x[button], button_y,
                           button_w, 1, buttons[button], TRUE, FALSE);
    wrefresh(nv_stdscr);
    usleep(NV_NCURSES_BUTTON_PRESS_TIME);

    /* restore the footer */

    nv_ncurses_set_footer(d, NV_NCURSES_DEFAULT_FOOTER_LEFT,
                          NV_NCURSES_DEFAULT_FOOTER_RIGHT);

    /* clean up */

    if (t_pager) {
        nv_ncurses_free_text_rows(t_pager);
    }
    if (p) {
        nv_ncurses_destroy_pager(p);
    }
    nv_ncurses_destroy_region(d->message);
    d->message = NULL;

    wrefresh(nv_stdscr);

    return button;
}





/*****************************************************************************/
/*
 * helper functions for the progress bar
 */

static int choose_char(int i, int p[4], char v[4], char def)
{
    if (p[0] == i) return v[0] ? v[0] : def;
    if (p[1] == i) return v[1] ? v[1] : def;
    if (p[2] == i) return v[2] ? v[2] : def;
    if (p[3] == i) return v[3] ? v[3] : def;
    return def;
}

static void init_percentage_string(char v[4], int n)
{
    int j;

    n = NV_MAX(n, 1);

    v[0] = (n/100);
    v[1] = (n/10) - (v[0]*10);
    v[2] = (n - (v[0]*100) - (v[1]*10));
    v[0] += '0';
    v[1] += '0';
    v[2] += '0';
    v[3] = '%';

    for (j = 0; j < 3; j++) {
        if (v[j] == '0') v[j] = 0;
        else break;
    }
}

static void init_position(int p[4], int w)
{
    p[0] = 0 + (w - 4)/2;
    p[1] = 1 + (w - 4)/2;
    p[2] = 2 + (w - 4)/2;
    p[3] = 3 + (w - 4)/2;
}



/*****************************************************************************/
/*
 * misc helper functions
 */


/*
 * nv_ncurses_create_command_list_text() - build a string from the command list
 */

static char *nv_ncurses_create_command_list_text(DataStruct *d, CommandList *cl)
{
    int i, len;
    Command *c;
    char *str, *perms, *ret = strdup("");

    for (i = 0; i < cl->num; i++) {
        c = &cl->cmds[i];
        
        str = NULL;

        switch (c->cmd) {
            
        case INSTALL_CMD:
            perms = nv_ncurses_mode_to_permission_string(c->mode);
            len = strlen(c->s0) + strlen(c->s1) + strlen(perms) + 64;
            if (c->s2) {
                len += strlen(c->s2) + 64;
            }
            str = (char *) malloc(len + 1);
            snprintf(str, len, "Install the file '%s' as '%s' with "
                     "permissions '%s'", c->s0, c->s1, perms);
            free(perms);
            if (c->s2) {
                len = strlen(c->s2) + 64;
                snprintf(str + strlen(str), len,
                         " then execute the command `%s`", c->s2);
            }
            break;
            
        case RUN_CMD:
            len = strlen(c->s0) + 64;
            str = (char *) malloc(len + 1);
            snprintf(str, len, "Execute the command `%s`", c->s0);
            break;
            
        case SYMLINK_CMD:
            len = strlen(c->s0) + strlen(c->s1) + 64;
            str = (char *) malloc(len + 1);
            snprintf(str, len, "Create a symbolic link '%s' to '%s'",
                     c->s0, c->s1);
            break;
            
        case BACKUP_CMD:
            len = strlen(c->s0) + 64;
            str = (char *) malloc(len + 1);
            snprintf(str, len, "Backup the file '%s'", c->s0);
            break;

        case DELETE_CMD:
            len = strlen(c->s0) + 64;
            str = (char *) malloc(len + 1);
            snprintf(str, len, "Delete the file '%s'", c->s0);
            break;

        default:
            /* XXX should not get here */
            break;
        }

        if (str) {
            int lenret, lenstr;
            char *tmp;

            lenret = strlen(ret);
            lenstr = strlen(str);
            tmp = malloc(lenret + lenstr + 2 /* "\n\0" */);

            tmp[0] = 0;

            strncat(tmp, ret, lenret);
            strncat(tmp, str, lenstr);

            tmp[lenret + lenstr] = '\n';
            tmp[lenret + lenstr + 1] = 0;

            free(str);
            free(ret);
            ret = tmp;
        }
    }

    return ret;

}


/*
 * mode_to_permission_string() - given a mode bitmask, allocate and
 * write a permission string.
 */

static char *nv_ncurses_mode_to_permission_string(mode_t mode)
{
    char *s = (char *) malloc(10);
    memset (s, '-', 9);
    
    if (mode & (1 << 8)) s[0] = 'r';
    if (mode & (1 << 7)) s[1] = 'w';
    if (mode & (1 << 6)) s[2] = 'x';
    
    if (mode & (1 << 5)) s[3] = 'r';
    if (mode & (1 << 4)) s[4] = 'w';
    if (mode & (1 << 3)) s[5] = 'x';
    
    if (mode & (1 << 2)) s[6] = 'r';
    if (mode & (1 << 1)) s[7] = 'w';
    if (mode & (1 << 0)) s[8] = 'x';
    
    s[9] = '\0';
    return s;

} /* mode_to_permission_string() */




/*
 * nv_free_text_rows() - free the TextRows data structure allocated by
 * nv_format_text_rows()
 */

static void nv_ncurses_free_text_rows(TextRows *t)
{
    int i;
    
    if (!t) return;
    for (i = 0; i < t->n; i++) free(t->t[i]);
    if (t->t) free(t->t);
    free(t);

} /* nv_free_text_rows() */



/*
 * nv_ncurses_formatted_printw() - this function formats the string
 * str on the region region, wrapping to the next line as necessary,
 * using the bounding box specified by x, y, w, and h.
 *
 * The number of lines printed are returned.
 */

static int nv_ncurses_format_print(DataStruct *d, RegionStruct *region,
                                   int x, int y, int w, int h,
                                   TextRows *t)
{
    int i, n;
    
    n = NV_MIN(t->n, h);
    
    wattrset(nv_stdscr, region->attr);

    for (i = 0; i < n; i++) {
        mvwaddstr(nv_stdscr, region->y + y + i, region->x + x, t->t[i]);
    }
    
    wattrset(nv_stdscr, A_NORMAL);
    return n;

} /* nv_ncurses_format_printw() */



/*
 * nv_ncurses_check_resize() - check if the dimensions of the screen
 * have changed; if so, update the old header and footer regions,
 * clear everything, update our cached dimensions, and recreate the
 * header and footer.
 *
 * XXX we could catch the SIGWINCH (window resize) signal, but that's
 * asynchronous... it's safer to only attempt to handle a resize when
 * we know we can.
 */

static int nv_ncurses_check_resize(DataStruct *d, bool force)
{
    int x, y;

    getmaxyx(nv_stdscr, y, x);

    if (!force) {
        if ((x == d->width) && (y == d->height)) {
            /* no resize detected... just return */
            return FALSE;
        }
    }

    /* we have been resized */

    /* destroy the old header and footer */

    nv_ncurses_destroy_region(d->header);
    nv_ncurses_destroy_region(d->footer);

    wclear(nv_stdscr);
    
    /* update our cached copy of the dimensions */

    d->height = y;
    d->width = x;

    /* recreate the header and footer */
    
    d->header = nv_ncurses_create_region(d, 1, 0, d->width - 2, 1,
                                         NV_NCURSES_HEADER_COLOR,
                                         NV_NCURSES_HEADER_NO_COLOR);

    d->footer = nv_ncurses_create_region(d, 1, d->height - 2, d->width - 2, 1,
                                         NV_NCURSES_FOOTER_COLOR,
                                         NV_NCURSES_FOOTER_NO_COLOR);
    
    nv_ncurses_set_header(d, d->title);
    nv_ncurses_set_footer(d, d->footer_left, d->footer_right);
    
    return TRUE;

} /* nv_ncurses_check_resize() */
