/************************************************************************
*                                                                       *
*             Star Traders: A Game of Interstellar Trading              *
*                Copyright (C) 1990-2026, John Zaitseff                 *
*                                                                       *
************************************************************************/

/*
  Author: John Zaitseff <J.Zaitseff@zap.org.au>
  SPDX-License-Identifier: GPL-3.0-or-later
  $Id: fae2a2573bf255da38974518f457dd9135bce737 $

  This file, intf.c, contains the actual implementation of basic text
  input/output routines as used in Star Traders.


  This program is free software: you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation, either version 3 of the License, or (at your
  option) any later version.

  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 https://www.gnu.org/licenses/.
*/


#include "trader.h"


/************************************************************************
*                      Global variable definitions                      *
************************************************************************/

WINDOW *curwin = NULL;		// Top-most (current) window
bool use_color = true;		// True to use colour


// Character renditions (attributes) used by Star Traders

chtype attr_root_window;	// Root window (behind all others)
chtype attr_game_title;		// One-line game title at top

chtype attr_normal_window;	// Normal window background
chtype attr_title;		// Normal window title
chtype attr_subtitle;		// Normal window subtitle
chtype attr_normal;		// Normal window text
chtype attr_highlight;		// Normal window highlighted string
chtype attr_blink;		// Blinking text in normal window
chtype attr_keycode;		// Make keycodes like <1> stand out
chtype attr_choice;		// Make map/company choices stand out
chtype attr_input_field;	// Background for input text field
chtype attr_waitforkey;		// "Press any key", normal window

chtype attr_map_window;		// Map window background
chtype attr_mapwin_title;	// Map window title (player name, turn)
chtype attr_mapwin_highlight;	// Map window title highlight
chtype attr_mapwin_blink;	// Map window title blinking text
chtype attr_map_empty;		// On map, empty space
chtype attr_map_outpost;	// On map, outpost
chtype attr_map_star;		// On map, star
chtype attr_map_company;	// On map, company
chtype attr_map_choice;		// On map, a choice of moves

chtype attr_status_window;	// Status window background

chtype attr_error_window;	// Error message window background
chtype attr_error_title;	// Error window title
chtype attr_error_normal;	// Error window ordinary text
chtype attr_error_highlight;	// Error window highlighted string
chtype attr_error_waitforkey;	// "Press any key", error window


/************************************************************************
*               Game printing global variable definitions               *
************************************************************************/

wchar_t	*keycode_company;		// Keycodes for each company
wchar_t	*printable_map_val;		// Printable output for each map value
chtype	*chtype_map_val[MAX_COMPANIES + 3];	// as chtype strings

wchar_t	*keycode_game_move;		// Keycodes for each game move
wchar_t	*printable_game_move;		// Printable output for each game move
chtype	*chtype_game_move[NUMBER_MOVES];	// as chtype strings


/************************************************************************
*        Module-specific constants, type declarations and macros        *
************************************************************************/

typedef struct txwin {
    WINDOW		*win;		// Pointer to window structure
    struct txwin	*next;		// Next window in stack
    struct txwin	*prev;		// Previous window in stack
} txwin_t;


// Initialisation macros used in init_screen()

#define __stringify(s) #s

#define init_game_str(_var, _default, _checkpos)			\
    do {								\
	char *s = gettext(_default);					\
	if (xmbstowcs(buf, s, BUFSIZE) < (_checkpos) + 1		\
	    || buf[_checkpos] != L'|') {				\
	    err_exit(_("%s: string has incorrect format: '%s'"),	\
		     __stringify(_var), s);				\
	}								\
	(_var) = xwcsdup(buf);						\
	(_var)[_checkpos] = L'\0';					\
    } while (0)

#define init_game_chstr(_chvar, _var, _attr, _err)			\
    do {								\
	chtype *p = chbuf;						\
	wchar_t c;							\
	int w, n;							\
	mbstate_t mbstate;						\
									\
	c = (_var);							\
	if ((w = wcwidth(c)) < 1) {					\
	    err_exit(_("%s: character has illegal width: '%lc'"),	\
		     __stringify(_err), (wint_t) c);			\
	}								\
									\
	memset(&mbstate, 0, sizeof(mbstate));				\
	n = xwcrtomb(convbuf, c, &mbstate);				\
	for (int i = 0; i < n; i++) {					\
	    *p++ = (unsigned char) convbuf[i] | (_attr);		\
	}								\
									\
	if (w == 1) {							\
	    n = xwcrtomb(convbuf, L' ', &mbstate);			\
	    for (int i = 0; i < n; i++) {				\
		*p++ = (unsigned char) convbuf[i] | attr_map_empty;	\
	    }								\
	}								\
									\
	n = xwcrtomb(convbuf, L'\0', &mbstate);				\
	for (int i = 0; i < n; i++) {					\
	    *p++ = (unsigned char) convbuf[i];				\
	}								\
									\
	(_chvar) = xchstrdup(chbuf);					\
    } while (0)


// Declarations for argument processing in mkchstr()

#define MAXFMTARGS	8	// Maximum number of positional arguments

enum argument_type {
    TYPE_NONE,			// No type yet assigned
    TYPE_CHAR,			// char
    TYPE_WCHAR,			// wint_t
    TYPE_INT,			// int
    TYPE_LONGINT,		// long int
    TYPE_DOUBLE,		// double
    TYPE_STRING,		// const char *
    TYPE_WSTRING		// const wchar_t *
};

struct argument {
    enum argument_type a_type;
    union a {
	char		a_char;
	wint_t		a_wchar;
	int		a_int;
	long int	a_longint;
	double		a_double;
	const char	*a_string;
	const wchar_t	*a_wstring;
    } a;
};


#define MAXFMTSPECS	16	// Maximum number of conversion specifiers

struct convspec {
    wchar_t	spec;		// Conversion specifier: c d f N s
    int		arg_num;	// Which variable argument to use
    ptrdiff_t	len;		// Length of conversion specifier, 0 = unused
    int		precision;	// Precision value
    bool	flag_group;	// Flag "'" (thousands grouping)
    bool	flag_nosym;	// Flag "!" (omit currency symbol)
    bool	flag_prec;	// Flag "." (precision)
    bool	flag_long;	// Length modifier "l" (long)
};


/************************************************************************
*                       Module-specific variables                       *
************************************************************************/

txwin_t *topwin   = NULL;	// Top-most txwin structure
txwin_t *firstwin = NULL;	// First (bottom-most) txwin structure


/************************************************************************
*                  Module-specific function prototypes                  *
************************************************************************/

/*
  Function:   init_title - Draw the main window title
  Parameters: (none)
  Returns:    (nothing)

  This function draws the main window game title, "Star Traders", and
  clears the rest of the screen.  It does NOT call wrefresh().
*/
static void init_title (void);


/*
  Function:   sigterm_handler - Handle program termination signals
  Parameters: sig             - Signal number
  Returns:    (nothing)

  This function handles termination signals (like SIGINT, SIGTERM and
  SIGQUIT) by clearing the screen, uninstalling itself and reraising the
  signal.
*/
static void sigterm_handler (int sig);


/*
  Function:   txresize - Handle a terminal resize event
  Parameters: (none)
  Returns:    (nothing)

  This function handles a SIGWINCH (terminal window size changed) event
  by refreshing Curses windows as appropriate.
*/
#ifdef HANDLE_RESIZE_EVENTS
static void txresize (void);
#endif


/*
  Function:   mkchstr_parse - Parse the format string for mkchstr()
  Parameters: format        - Format string as described for mkchstr()
              format_arg    - Pointer to variable arguments array
              format_spec   - Pointer to conversion specifiers array
              args          - Variable argument list passed to mkchstr()
  Returns:    int           - 0 if OK, -1 if error (with errno set)

  This helper function parses the format string passed to mkchstr(),
  setting the format_arg and format_spec arrays appropriately.
*/
static int mkchstr_parse (const wchar_t *restrict format,
			  struct argument *restrict format_arg,
			  struct convspec *restrict format_spec,
			  va_list args);


/*
  Function:   mkchstr_add  - Add one character to the mkchstr() buffers
  Parameters: outbuf       - Pointer to wchar_t pointer in which to store char
              attrbuf      - Pointer to chtype pointer in which to store attr
              count        - Pointer to number of wchar_t elements left in outbuf
              attr         - Character rendition to use
              maxlines     - Maximum number of screen lines to use
              maxwidth     - Maximum width of each line, in column positions
              line         - Pointer to current line number
              width        - Pointer to current line width
              lastspc      - Pointer to wchar_t * pointer to last space
              spcattr      - Pointer to corresponding place in attrbuf
              widthspc     - Pointer to width just before last space
              widthbuf     - Pointer to buffer to store widths of each line
              widthbufsize - Number of int elements in widthbuf
              str          - Pointer to const wchar_t * pointer to string
  Returns:    int          - -1 on error (with errno set), 0 otherwise

  This helper function adds one wide character from **str to **outbuf,
  and the character rendition attr to **attrbuf, incrementing *str and
  *outbuf and decrementing *count.  If a string is too long for the
  current line, a previous space in the current line is converted to a
  new line (if possible), else a new line is inserted into the current
  location (if not on the last line).  *line, *width, *lastspc, *widthspc
  and widthbuf[] are all updated appropriately.
*/
static int mkchstr_add (wchar_t *restrict *restrict outbuf,
			chtype *restrict *restrict attrbuf,
			int *restrict count, chtype attr, int maxlines,
			int maxwidth, int *restrict line, int *restrict width,
			wchar_t *restrict *restrict lastspc,
			chtype *restrict *restrict spcattr,
			int *restrict widthspc, int *restrict widthbuf,
			int widthbufsize,
			const wchar_t *restrict *restrict str);


/*
  Function:   mkchstr_conv - Convert (wcbuf, attrbuf) to chbuf
  Parameters: chbuf        - Pointer to chtype buffer in which to store string
              chbufsize    - Number of chtype elements in chbuf
              wcbuf        - Wide-character string from which to convert
              attrbuf      - Associated character rendition array
  Returns:    (nothing)

  This helper function converts the wide-character string in wcbuf and
  the array of character renditions in attrbuf to a chtype * string.
*/
static void mkchstr_conv (chtype *restrict chbuf, int chbufsize,
			  wchar_t *restrict wcbuf, chtype *restrict attrbuf);


/*
  Function:   getwch - Get a wide character from the keyboard
  Parameters: win    - Window to use (should be curwin)
              wch    - Pointer to wide character result
  Returns:    int    - OK, KEY_CODE_YES or ERR

  This internal function waits for a complete wide character to be typed
  on the keyboard.  OK is returned if wch contains an ordinary wide
  character, KEY_CODE_YES if a function key or control key, or ERR on
  error.

  This function is either a wrapper (with modifications) for wget_wch()
  from Curses, or an implementation of that function using wgetch().
*/
static int getwch (WINDOW *win, wint_t *restrict wch);


/*
  Function:   cpos_end - Adjust cpos and st for printing the ending part of buf
  Parameters: buf      - Pointer to current editing buffer
              cpos     - Pointer to current cursor position (result)
              st       - Pointer to current starting offset for buf[] (result)
              clen     - Current column width of entire string
              width    - Width of editing field in column spaces
              len      - Length of string being edited (wchar_t elements)
  Returns:    (nothing)

  This helper function adjusts *cpos and *st so that the cursor is placed
  at the end of the current editing buffer buf[].
*/
static void cpos_end (const wchar_t *restrict buf, int *restrict cpos,
		      int *restrict st, int clen, int width, int len);


/*
  Function:   cpos_dec - Adjust cpos and st: scroll to the left by w columns
  Parameters: buf      - Pointer to current editing buffer
              cpos     - Pointer to current cursor position (result)
              st       - Pointer to current starting offset for buf[] (result)
              w        - Number of columns to scroll left
              width    - Width of editing field in column spaces
  Returns:    (nothing)

  This helper function adjusts *cpos and *st so that the cursor is moved
  to the left by w column positions.
*/
static void cpos_dec (const wchar_t *restrict buf, int *restrict cpos,
		      int *restrict st, int w, int width);


/*
  Function:   cpos_inc - Adjust cpos and st: scroll to the right by w columns
  Parameters: buf      - Pointer to current editing buffer
              cpos     - Pointer to current cursor position (result)
              st       - Pointer to current starting offset for buf[] (result)
              w        - Number of columns to scroll right
              width    - Width of editing field in column spaces
  Returns:    (nothing)

  This helper function adjusts *cpos and *st so that the cursor is moved
  to the right by w column positions.
*/
static void cpos_inc (const wchar_t *restrict buf, int *restrict cpos,
		      int *restrict st, int w, int width);


/*
  Function:   txinput_fixup - Copy strings with fixup
  Parameters: dest          - Destination buffer of size BUFSIZE
              src           - Source buffer of size BUFSIZE
              isfloat       - True if src contains a floating point number
  Returns:    (nothing)

  This helper function copies the string in src to dest, performing
  certain fixups along the way.  In particular, thousands separators are
  removed and (if isfloat is true) the monetary radix (decimal point) is
  replaced by the normal one.

  This function is used by gettxdouble() and gettxlong() to share some
  common code.
*/
static void txinput_fixup (wchar_t *restrict dest, const wchar_t *restrict src,
			   bool isfloat);


/************************************************************************
*             Basic text input/output function definitions              *
************************************************************************/

/* These functions are documented either in the file "intf.h" or in the
   comments above. */


/***********************************************************************/
// init_screen: Initialise the screen (terminal display)

void init_screen (void)
{
    struct sigaction sa;


    // Initialise signal handlers
    sa.sa_handler = sigterm_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGQUIT);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
	errno_exit("sigaction(SIGINT)");
    }
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
	errno_exit("sigaction(SIGTERM)");
    }
    if (sigaction(SIGQUIT, &sa, NULL) == -1) {
	errno_exit("sigaction(SIGQUIT)");
    }

    // Initialise the screen
    initscr();

    if (COLS < MIN_COLS || LINES < MIN_LINES) {
	err_exit(_("terminal size is too small (%d x %d required)"),
		 MIN_COLS, MIN_LINES);
    }

    // Initialise variables controlling the stack of windows
    curwin = stdscr;
    topwin = NULL;
    firstwin = NULL;

    noecho();
    curs_set(CURS_OFF);
    raw();

    // Initialise all character renditions used in the game
    use_color = ! option_no_color && has_colors();
    if (use_color) {
	start_color();

	init_pair(1,  COLOR_BLACK,  COLOR_WHITE);
	init_pair(2,  COLOR_BLUE,   COLOR_BLACK);
	init_pair(3,  COLOR_GREEN,  COLOR_BLACK);
	init_pair(4,  COLOR_CYAN,   COLOR_BLUE);
	init_pair(5,  COLOR_RED,    COLOR_BLACK);
	init_pair(6,  COLOR_YELLOW, COLOR_BLACK);
	init_pair(7,  COLOR_YELLOW, COLOR_BLUE);
	init_pair(8,  COLOR_YELLOW, COLOR_CYAN);
	init_pair(9,  COLOR_WHITE,  COLOR_BLACK);
	init_pair(10, COLOR_WHITE,  COLOR_BLUE);
	init_pair(11, COLOR_WHITE,  COLOR_RED);

	attr_root_window      = COLOR_PAIR(9);
	attr_game_title	      = COLOR_PAIR(8)   | A_BOLD;

	attr_normal_window    = COLOR_PAIR(10);
	attr_title	      = COLOR_PAIR(6)   | A_BOLD;
	attr_subtitle	      = COLOR_PAIR(9);
	attr_normal	      = attr_normal_window;
	attr_highlight	      = COLOR_PAIR(7)   | A_BOLD;
	attr_blink	      = COLOR_PAIR(7)   | A_BOLD  | A_BLINK;
	attr_keycode	      = COLOR_PAIR(6)   | A_BOLD;
	attr_choice	      = COLOR_PAIR(11)  | A_BOLD;
	attr_input_field      = COLOR_PAIR(9);
	attr_waitforkey	      = COLOR_PAIR(4);

	attr_map_window	      = COLOR_PAIR(9);
	attr_mapwin_title     = COLOR_PAIR(10);
	attr_mapwin_highlight = COLOR_PAIR(7)   | A_BOLD;
	attr_mapwin_blink     = COLOR_PAIR(7)   | A_BOLD  | A_BLINK;
	attr_map_empty	      = COLOR_PAIR(2)   | A_BOLD;
	attr_map_outpost      = COLOR_PAIR(3)   | A_BOLD;
	attr_map_star	      = COLOR_PAIR(6)   | A_BOLD;
	attr_map_company      = COLOR_PAIR(5)   | A_BOLD;
	attr_map_choice	      = COLOR_PAIR(11)  | A_BOLD;

	attr_status_window    = COLOR_PAIR(1);

	attr_error_window     = COLOR_PAIR(11);
	attr_error_title      = COLOR_PAIR(6)   | A_BOLD;
	attr_error_normal     = attr_error_window;
	attr_error_highlight  = COLOR_PAIR(11)  | A_BOLD;
	attr_error_waitforkey = COLOR_PAIR(11);

    } else {
	// No colour is to be used

	attr_root_window      = A_NORMAL;
	attr_game_title	      = A_REVERSE | A_BOLD;

	attr_normal_window    = A_NORMAL;
	attr_title	      = A_REVERSE;
	attr_subtitle	      = A_REVERSE;
	attr_normal	      = attr_normal_window;
	attr_highlight	      = A_BOLD;
	attr_blink	      = A_BOLD | A_BLINK;
	attr_keycode	      = A_REVERSE;
	attr_choice	      = A_REVERSE;
	attr_input_field      = A_BOLD | '_';
	attr_waitforkey	      = A_NORMAL;

	attr_map_window	      = A_NORMAL;
	attr_mapwin_title     = A_NORMAL;
	attr_mapwin_highlight = A_BOLD;
	attr_mapwin_blink     = A_BOLD | A_BLINK;
	attr_map_empty	      = A_NORMAL;
	attr_map_outpost      = A_NORMAL;
	attr_map_star	      = A_BOLD;
	attr_map_company      = A_BOLD;
	attr_map_choice	      = A_REVERSE;

	attr_status_window    = A_REVERSE;

	attr_error_window     = A_REVERSE;
	attr_error_title      = A_BOLD;
	attr_error_normal     = attr_error_window;
	attr_error_highlight  = A_REVERSE;
	attr_error_waitforkey = A_REVERSE;
    }

    init_title();
    refresh();

    /* Initialise strings used for keycode input and map representations.

       Each string must have an ASCII vertical line (U+007C) in the
       correct position, followed by context information (such as
       "input|Company" and "output|MapVals").  This is done to overcome a
       limitation of gettext_noop() and N_() that does NOT allow context
       IDs.  This vertical line is replaced by a NUL character to
       terminate the resulting string.  The vertical line MAY appear in
       other positions; if so, it is handled correctly. */

    wchar_t *buf = xmalloc(BUFSIZE * sizeof(wchar_t));
    char convbuf[MB_LEN_MAX + 1];
    chtype chbuf[MB_LEN_MAX * 3 + 1];

    init_game_str(keycode_company, default_keycode_company, MAX_COMPANIES);
    init_game_str(keycode_game_move, default_keycode_game_move, NUMBER_MOVES);

    init_game_str(printable_map_val, default_printable_map_val, MAX_COMPANIES + 3);
    init_game_str(printable_game_move, default_printable_game_move, NUMBER_MOVES);

    /* To save time later, convert each output character to its own
       chtype string, with appropriate attributes. */

    init_game_chstr(chtype_map_val[MAP_TO_INDEX(MAP_EMPTY)],
		    printable_map_val[MAP_TO_INDEX(MAP_EMPTY)],
		    attr_map_empty, MAP_EMPTY);
    init_game_chstr(chtype_map_val[MAP_TO_INDEX(MAP_OUTPOST)],
		    printable_map_val[MAP_TO_INDEX(MAP_OUTPOST)],
		    attr_map_outpost, MAP_OUTPOST);
    init_game_chstr(chtype_map_val[MAP_TO_INDEX(MAP_STAR)],
		    printable_map_val[MAP_TO_INDEX(MAP_STAR)],
		    attr_map_star, MAP_STAR);
    for (int i = 0; i < MAX_COMPANIES; i++) {
	init_game_chstr(chtype_map_val[MAP_TO_INDEX(COMPANY_TO_MAP(i))],
			printable_map_val[MAP_TO_INDEX(COMPANY_TO_MAP(i))],
			attr_map_company, COMPANY_TO_MAP(i));
    }

    for (int i = 0; i < NUMBER_MOVES; i++) {
	init_game_chstr(chtype_game_move[i], printable_game_move[i],
			attr_map_choice, printable_game_move[i]);
    }

    free(buf);
}


/***********************************************************************/
// end_screen: Deinitialise the screen (terminal display)

void end_screen (void)
{
    delalltxwin();

    curs_set(CURS_ON);
    clear();
    refresh();
    endwin();

    curwin = NULL;
    topwin = NULL;
    firstwin = NULL;
}


/***********************************************************************/
// init_title: Draw the main window title

void init_title (void)
{
    bkgd(attr_root_window);
    attrset(attr_root_window);
    clear();

    mvwhline(stdscr, 0, 0, ' ' | attr_game_title, COLS);
    center(stdscr, 0, 0, attr_game_title, 0, 0, 1, _("Star Traders"));
}


/***********************************************************************/
// sigterm_handler: Handle program termination signals

void sigterm_handler (int sig)
{
    struct sigaction sa;


    /* The following Curses functions are NOT async-signal-safe (ie, are
       not reentrant) as they may well call malloc() or free().  However,
       it does allow us to terminate with the correct signal without
       having convoluted code in the main program. */

    curs_set(CURS_ON);
    clear();
    refresh();
    endwin();

    // Reraise the same signal, using the system-default handler
    sa.sa_handler = SIG_DFL;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    sigaction(sig, &sa, NULL);
    raise(sig);
}


/***********************************************************************/
// newtxwin: Create a new window, inserted into window stack

WINDOW *newtxwin (int nlines, int ncols, int begin_y, int begin_x,
		  bool dofill, chtype bkgd_attr)
{
    WINDOW *win;
    txwin_t *nw;


    // Centre the window, if required
    if (begin_y == WCENTER) {
	begin_y = (nlines == 0) ? 0 : (LINES - nlines) / 2;
    }
    if (begin_x == WCENTER) {
	begin_x = (ncols == 0) ? 0 : (COLS - ncols) / 2;
    }

    assert(nlines > 0);
    assert(ncols > 0);
    assert(begin_y >= 0);
    assert(begin_x >= 0);

    // Create the new window

    win = newwin(nlines, ncols, begin_y, begin_x);
    if (win == NULL) {
	err_exit_nomem();
    }

    // Insert the new window into the txwin stack

    nw = xmalloc(sizeof(txwin_t));
    nw->win = win;
    nw->next = NULL;
    nw->prev = topwin;

    if (topwin != NULL) {
	topwin->next = nw;
    }

    topwin = nw;
    curwin = win;

    if (firstwin == NULL) {
	firstwin = nw;
    }

    // Paint the background and border, if required

    if (dofill) {
	wbkgd(win, bkgd_attr);
	box(win, 0, 0);
    }

    if (! use_color) {
	wbkgdset(win, A_NORMAL);
    }

    return win;
}


/***********************************************************************/
// deltxwin: Delete the top-most window in the window stack

int deltxwin (void)
{
    txwin_t *cur, *prev;
    int ret;


    if (topwin == NULL) {
	return ERR;
    }

    // Remove window from the txwin stack

    cur = topwin;
    prev = topwin->prev;
    topwin = prev;

    if (prev != NULL) {
	prev->next = NULL;
	curwin = prev->win;
    } else {
	firstwin = NULL;
	curwin = stdscr;
    }

    ret = delwin(cur->win);
    free(cur);

    return ret;
}


/***********************************************************************/
// delalltxwin: Delete all windows in the window stack

int delalltxwin (void)
{
    while (topwin != NULL) {
	deltxwin();
    }

    return OK;
}


/***********************************************************************/
// txrefresh: Redraw all windows in the window stack

int txrefresh (void)
{
    touchwin(stdscr);
    wnoutrefresh(stdscr);

    for (txwin_t *p = firstwin; p != NULL; p = p->next) {
	touchwin(p->win);
	wnoutrefresh(p->win);
    }

    return doupdate();
}


/***********************************************************************/
// txresize: Handle a terminal resize event

#ifdef HANDLE_RESIZE_EVENTS

void txresize (void)
{
    /* The current implementation cannot resize windows per se: a given
       window would have to be destroyed and recreated in the new
       location, then redrawn, most likely via a call-back function.
       We just redraw the game title, refresh all windows and hope for
       the best! */

    init_title();
    txrefresh();
}

#endif // HANDLE_RESIZE_EVENTS


/***********************************************************************/
// txdlgbox: Display a dialog box and wait for any key

int txdlgbox (int maxlines, int ncols, int begin_y, int begin_x,
	      chtype bkgd_attr, chtype title_attr, chtype norm_attr,
	      chtype alt1_attr, chtype alt2_attr, chtype keywait_attr,
	      const char *restrict boxtitle, const char *restrict format, ...)
{
    bool usetitle = (boxtitle != NULL);

    chtype *chbuf;
    int *widthbuf;
    int lines;
    va_list args;


    assert(maxlines > 0);

    chbuf = xmalloc(BUFSIZE * sizeof(chtype));
    widthbuf = xmalloc(maxlines * sizeof(int));

    va_start(args, format);
    lines = vmkchstr(chbuf, BUFSIZE, norm_attr, alt1_attr, alt2_attr, maxlines,
		     ncols - 4, widthbuf, maxlines, format, args);
    va_end(args);

    newtxwin(usetitle ? lines + 6 : lines + 5, ncols, begin_y, begin_x,
	     true, bkgd_attr);

    if (usetitle) {
	center(curwin, 1, 0, title_attr, 0, 0, 1, boxtitle);
    }

    centerch(curwin, usetitle ? 3 : 2, 0, chbuf, lines, widthbuf);
    wait_for_key(curwin, getmaxy(curwin) - 2, keywait_attr);
    deltxwin();

    free(widthbuf);
    free(chbuf);
    return OK;
}


/***********************************************************************/
// mkchstr: Prepare a string for printing to screen

int mkchstr (chtype *restrict chbuf, int chbufsize, chtype attr_norm,
	     chtype attr_alt1, chtype attr_alt2, int maxlines, int maxwidth,
	     int *restrict widthbuf, int widthbufsize,
	     const char *restrict format, ...)
{
    va_list args;
    int lines;


    va_start(args, format);
    lines = vmkchstr(chbuf, chbufsize, attr_norm, attr_alt1, attr_alt2,
		     maxlines, maxwidth, widthbuf, widthbufsize, format, args);
    va_end(args);
    return lines;
}


/***********************************************************************/
// mkchstr_parse: Parse the format string for mkchstr()

int mkchstr_parse (const wchar_t *restrict format,
		   struct argument *restrict format_arg,
		   struct convspec *restrict format_spec, va_list args)
{
    int num_args = 0;			// 0 .. MAXFMTARGS
    int arg_num = 0;			// Current index into format_arg[]
    int specs_left = MAXFMTSPECS;	// MAXFMTSPECS .. 0 (counting down)


    memset(format_arg, 0, MAXFMTARGS * sizeof(format_arg[0]));
    memset(format_spec, 0, MAXFMTSPECS * sizeof(format_spec[0]));

    while (*format != L'\0') {
	switch (*format++) {
	case L'^':
	    // Switch to a different character rendition
	    if (*format == L'\0') {
		errno = EINVAL;
		return -1;
	    } else {
		// Ignore next character for now
		format++;
	    }
	    break;

	case L'%':
	    // Process a conversion specifier
	    if (*format == L'\0') {
		errno = EINVAL;
		return -1;
	    } else if (*format == L'%') {
		// Ignore "%%" specifier for now
		format++;
	    } else {
		const wchar_t *start = format;
		enum argument_type arg_type;
		bool inspec = true;
		bool flag_posn = false;		// Have we already seen "$"?
		bool flag_other = false;	// Have we seen something else?
		int count = 0;

		while (inspec && *format != L'\0') {
		    wchar_t c = *format++;
		    switch (c) {
		    case L'0':
			// Zero flag, or part of numeric count
			if (count == 0) {
			    // Zero flag is NOT supported
			    errno = EINVAL;
			    return -1;
			}

			count *= 10;
			break;

		    case L'1':
		    case L'2':
		    case L'3':
		    case L'4':
		    case L'5':
		    case L'6':
		    case L'7':
		    case L'8':
		    case L'9':
			// Part of some numeric count
			count = count * 10 + (c - L'0');
			break;

		    case L'$':
			// Fixed-position argument
			if (flag_posn || flag_other || count == 0) {
			    errno = EINVAL;
			    return -1;
			}

			if (count > MAXFMTARGS) {
			    errno = E2BIG;
			    return -1;
			}

			flag_posn = true;
			arg_num = count - 1;
			count = 0;
			break;

		    case L'\'':
			// Use locale-specific thousands group separator
			if (format_spec->flag_group) {
			    errno = EINVAL;
			    return -1;
			}

			format_spec->flag_group = true;
			flag_other = true;
			break;

		    case L'!':
			// Omit the locale-specific currency symbol
			if (format_spec->flag_nosym) {
			    errno = EINVAL;
			    return -1;
			}

			format_spec->flag_nosym = true;
			flag_other = true;
			break;

		    case L'.':
			// Precision flag
			if (format_spec->flag_prec || count != 0) {
			    errno = EINVAL;
			    return -1;
			}

			format_spec->flag_prec = true;
			flag_other = true;
			break;

		    case L'l':
			// Long length modifier
			if (format_spec->flag_long) {
			    // "ll" is NOT supported
			    errno = EINVAL;
			    return -1;
			}

			format_spec->flag_long = true;
			flag_other = true;
			break;

		    case L'c':
			// Insert a character (char or wchar_t)
			if (format_spec->flag_group || format_spec->flag_nosym
			    || format_spec->flag_prec || count != 0) {
			    errno = EINVAL;
			    return -1;
			}

			arg_type = format_spec->flag_long ?
			    TYPE_WCHAR : TYPE_CHAR;
			goto handlefmt;

		    case L'd':
			// Insert an integer (int or long int)
			if (format_spec->flag_nosym || format_spec->flag_prec
			    || count != 0) {
			    errno = EINVAL;
			    return -1;
			}

			arg_type = format_spec->flag_long ?
			    TYPE_LONGINT : TYPE_INT;
			goto handlefmt;

		    case L'f':
			// Insert a floating-point number (double)
			if (format_spec->flag_nosym || format_spec->flag_long ||
			    (! format_spec->flag_prec && count != 0)) {
			    errno = EINVAL;
			    return -1;
			}

			format_spec->precision = count;

			arg_type = TYPE_DOUBLE;
			goto handlefmt;

		    case L'N':
			// Insert a monetary amount (double)
			if (format_spec->flag_group || format_spec->flag_prec
			    || format_spec->flag_long || count != 0) {
			    errno = EINVAL;
			    return -1;
			}

			arg_type = TYPE_DOUBLE;
			goto handlefmt;

		    case L's':
			// Insert a string (const char * or const wchar_t *)
			if (format_spec->flag_group || format_spec->flag_nosym
			    || format_spec->flag_prec || count != 0) {
			    errno = EINVAL;
			    return -1;
			}

			arg_type = format_spec->flag_long ?
			    TYPE_WSTRING : TYPE_STRING;

		    handlefmt:
			if (arg_num >= MAXFMTARGS || specs_left == 0) {
			    errno = E2BIG;
			    return -1;
			}

			if (format_arg[arg_num].a_type == TYPE_NONE) {
			    format_arg[arg_num].a_type = arg_type;
			} else if (format_arg[arg_num].a_type != arg_type) {
			    errno = EINVAL;
			    return -1;
			}

			format_spec->len = format - start;
			format_spec->arg_num = arg_num;
			format_spec->spec = c;

			arg_num++;
			num_args = MAX(num_args, arg_num);

			format_spec++;
			specs_left--;

			inspec = false;
			break;

		    default:
			errno = EINVAL;
			return -1;
		    }
		}
		if (inspec) {
		    errno = EINVAL;
		    return -1;
		}
	    }
	    break;

	default:
	    // Process an ordinary character: do nothing for now
	    ;
	}
    }

    for (int i = 0; i < num_args; format_arg++, i++) {
	switch (format_arg->a_type) {
	case TYPE_CHAR:
	    format_arg->a.a_char = (char) va_arg(args, int);
	    break;

	case TYPE_WCHAR:
	    format_arg->a.a_wchar =
		(wint_t) (sizeof(wint_t) < sizeof(int) ?
			  va_arg(args, int) :
			  va_arg(args, wint_t));
	    break;

	case TYPE_INT:
	    format_arg->a.a_int = va_arg(args, int);
	    break;

	case TYPE_LONGINT:
	    format_arg->a.a_longint = va_arg(args, long int);
	    break;

	case TYPE_DOUBLE:
	    format_arg->a.a_double = va_arg(args, double);
	    break;

	case TYPE_STRING:
	    format_arg->a.a_string = va_arg(args, const char *);
	    break;

	case TYPE_WSTRING:
	    format_arg->a.a_wstring = va_arg(args, const wchar_t *);
	    break;

	default:
	    /* Cannot allow unused arguments, as we have no way of
	       knowing how much space they take (cf. int vs. long long
	       int). */
	    errno = EINVAL;
	    return -1;
	}
    }

    return 0;
}


/***********************************************************************/
// mkchstr_add: Add a character to the mkchstr buffer

int mkchstr_add (wchar_t *restrict *restrict outbuf,
		 chtype *restrict *restrict attrbuf, int *restrict count,
		 chtype attr, int maxlines, int maxwidth, int *restrict line,
		 int *restrict width, wchar_t *restrict *restrict lastspc,
		 chtype *restrict *restrict spcattr, int *restrict widthspc,
		 int *restrict widthbuf,
		 int widthbufsize __attribute__((unused)),
		 const wchar_t *restrict *restrict str)
{
    int w, wspc;


    if (*line < 0) {
	// First character in buffer: start line 0
	*line = 0;
    }

    if (**str == L'\n') {
	// Start a new line

	if (*line < maxlines - 1) {
	    *(*outbuf)++ = L'\n';
	    *(*attrbuf)++ = 0;
	    (*count)--;
	}

	widthbuf[*line] = *width;
	*width = 0;

	*lastspc = NULL;
	*spcattr = NULL;
	*widthspc = 0;

	(*line)++;
	(*str)++;
    } else {
	w = wcwidth(**str);
	if (w < 0) {
	    // We don't support control or non-printable characters
	    errno = EILSEQ;
	    return -1;
	}

	if (*width + w > maxwidth) {
	    // Current line would be too long to fit in **str

	    if (! iswspace(**str) && *lastspc != NULL && *line < maxlines - 1) {
		// Break on the last space in this line
		wspc = wcwidth(**lastspc);

		**lastspc = L'\n';
		**spcattr = 0;

		widthbuf[*line] = *widthspc;
		*width -= *widthspc + wspc;

		*lastspc = NULL;
		*spcattr = NULL;
		*widthspc = 0;

		(*line)++;
	    } else {
		// Insert a new-line character (if not on last line)
		if (*line < maxlines - 1) {
		    *(*outbuf)++ = L'\n';
		    *(*attrbuf)++ = 0;
		    (*count)--;
		}

		widthbuf[*line] = *width;
		*width = 0;

		*lastspc = NULL;
		*spcattr = NULL;
		*widthspc = 0;

		(*line)++;

		/* Skip any following spaces.  This assumes that no-one
		   will ever have combining diacritical marks following a
		   (line-breaking) space! */
		while (iswspace(**str)) {
		    if (*(*str)++ == L'\n') {
			break;
		    }
		}
	    }
	} else {
	    // Insert an ordinary character into the output buffer

	    if (iswspace(**str)) {
		*lastspc = *outbuf;
		*spcattr = *attrbuf;
		*widthspc = *width;
	    }

	    *(*outbuf)++ = **str;
	    *(*attrbuf)++ = attr;
	    (*count)--;
	    *width += w;
	    (*str)++;
	}
    }

    return 0;
}


/***********************************************************************/
// mkchstr_conv: Convert (wcbuf, attrbuf) to chbuf

void mkchstr_conv (chtype *restrict chbuf, int chbufsize,
		   wchar_t *restrict wcbuf, chtype *restrict attrbuf)
{
    char convbuf[MB_LEN_MAX + 1];
    char endbuf[MB_LEN_MAX];
    mbstate_t mbstate, mbcopy;
    size_t endsize, n;
    char *p;
    bool done;


    memset(&mbstate, 0, sizeof(mbstate));
    done = false;
    while (! done) {
	// Make sure we always have enough space for ending shift sequence
	memcpy(&mbcopy, &mbstate, sizeof(mbstate));
	endsize = wcrtomb(endbuf, L'\0', &mbcopy);
	if (endsize == (size_t) -1) {
	    errno_exit(_("mkchstr_conv: NUL"));
	}

	// Yes, we want to convert a wide NUL, too!
	n = xwcrtomb(convbuf, *wcbuf, &mbstate);

	if (chbufsize > (int) endsize + (int) n) {
	    for (p = convbuf; n > 0; n--, p++, chbuf++, chbufsize--) {
		if (*p == '\0' || *p == '\n') {
		    /* This code assumes '\n' can never appear in a
		       multibyte string except as a control character---
		       which is true of all multibyte encodings (I
		       believe!) */
		    *chbuf = (unsigned char) *p;
		} else {
		    *chbuf = (unsigned char) *p | *attrbuf;
		}
	    }
	} else {
	    // Not enough space for *wcbuf, so terminate chbuf early
	    for (p = endbuf; endsize > 0; endsize--, p++, chbuf++) {
		*chbuf = (unsigned char) *p;
	    }
	    break;
	}

	done = (*wcbuf == L'\0');
	wcbuf++;
	attrbuf++;
    }
}


/***********************************************************************/
// vmkchstr: Prepare a string for printing to screen

int vmkchstr (chtype *restrict chbuf, int chbufsize, chtype attr_norm,
	      chtype attr_alt1, chtype attr_alt2, int maxlines, int maxwidth,
	      int *restrict widthbuf, int widthbufsize,
	      const char *restrict format, va_list args)
{
    struct argument format_arg[MAXFMTARGS];
    struct convspec format_spec[MAXFMTSPECS];
    struct convspec *spec;
    const wchar_t *wcformat;
    wchar_t *orig_wcformat;

    wchar_t *outbuf, *orig_outbuf;
    chtype *attrbuf, *orig_attrbuf;
    wchar_t *fmtbuf;

    int count, line, width;
    wchar_t *lastspc;
    chtype *spcattr;
    int widthspc;
    chtype curattr;
    int saved_errno;


    assert(chbuf != NULL);
    assert(chbufsize > 0);
    assert(chbufsize <= BUFSIZE);
    assert(maxlines > 0);
    assert(maxwidth > 0);
    assert(widthbuf != NULL);
    assert(widthbufsize >= maxlines);
    assert(format != NULL);

    outbuf = orig_outbuf = xmalloc(BUFSIZE * sizeof(wchar_t));
    attrbuf = orig_attrbuf = xmalloc(BUFSIZE * sizeof(chtype));
    wcformat = orig_wcformat = xmalloc(chbufsize * sizeof(wchar_t));
    fmtbuf = xmalloc(BUFSIZE * sizeof(wchar_t));

    // Convert format to a wide-character string
    xmbstowcs(orig_wcformat, format, BUFSIZE);

    if (mkchstr_parse(wcformat, format_arg, format_spec, args) < 0) {
	goto error;
    }

    // Construct the (outbuf, attrbuf) pair of arrays

    spec = format_spec;

    curattr = attr_norm;
    count = BUFSIZE;			// Space left in outbuf
    line = -1;				// Current line number (0 = first)
    width = 0;				// Width of the current line

    lastspc = NULL;			// Pointer to last space in line
    spcattr = NULL;			// Equivalent in attrbuf
    widthspc = 0;			// Width of line before last space

    while (*wcformat != L'\0' && count > 1 && line < maxlines) {
	switch (*wcformat) {
	case L'^':
	    // Switch to a different character rendition
	    if (*++wcformat == L'\0') {
		goto error_inval;
	    } else {
		switch (*wcformat) {
		case L'^':
		    if (mkchstr_add(&outbuf, &attrbuf, &count, curattr,
				    maxlines, maxwidth, &line, &width,
				    &lastspc, &spcattr, &widthspc, widthbuf,
				    widthbufsize, &wcformat) < 0) {
			goto error;
		    }
		    break;

		case L'{':
		    curattr = attr_alt1;
		    wcformat++;
		    break;

		case L'[':
		    curattr = attr_alt2;
		    wcformat++;
		    break;

		case L'}':
		case L']':
		    curattr = attr_norm;
		    wcformat++;
		    break;

		default:
		    goto error_inval;
		}
	    }
	    break;

	case L'%':
	    // Process a conversion specifier
	    if (*++wcformat == L'\0') {
		goto error_inval;
	    } else if (*wcformat == L'%') {
		if (mkchstr_add(&outbuf, &attrbuf, &count, curattr, maxlines,
				maxwidth, &line, &width, &lastspc, &spcattr,
				&widthspc, widthbuf, widthbufsize, &wcformat)
		    < 0) {
		    goto error;
		}
	    } else {
		assert(spec->len != 0);
		const wchar_t *str;
		wint_t wc;

		switch (spec->spec) {
		case L'c':
		    // Insert a character (char or wchar_t) into the output
		    if (spec->flag_long) {
			wc = format_arg[spec->arg_num].a.a_wchar;
		    } else {
			wc = btowc(format_arg[spec->arg_num].a.a_char);
		    }

		    if (wc == L'\0' || wc == WEOF) {
			wc = EILSEQ_REPL_WC;
		    }

		    fmtbuf[0] = wc;
		    fmtbuf[1] = L'\0';

		    str = fmtbuf;
		    goto insertstr;

		case L'd':
		    // Insert an integer (int or long int) into the output
		    if (spec->flag_long) {
			if (swprintf(fmtbuf, BUFSIZE, spec->flag_group ?
				     L"%'ld" : L"%ld",
				     format_arg[spec->arg_num].a.a_longint) < 0)
			    goto error;
		    } else {
			if (swprintf(fmtbuf, BUFSIZE, spec->flag_group ?
				     L"%'d" : L"%d",
				     format_arg[spec->arg_num].a.a_int) < 0)
			    goto error;
		    }

		    str = fmtbuf;
		    goto insertstr;

		case L'f':
		    // Insert a floating-point number (double) into the output
		    if (spec->flag_prec) {
			if (swprintf(fmtbuf, BUFSIZE, spec->flag_group ?
				     L"%'.*f" : L"%.*f", spec->precision,
				     format_arg[spec->arg_num].a.a_double) < 0)
			    goto error;
		    } else {
			if (swprintf(fmtbuf, BUFSIZE, spec->flag_group ?
				     L"%'f" : L"%f",
				     format_arg[spec->arg_num].a.a_double) < 0)
			    goto error;
		    }

		    str = fmtbuf;
		    goto insertstr;

		case L'N':
		    // Insert a monetary amount (double) into the output
		    if (xwcsfmon(fmtbuf, BUFSIZE, spec->flag_nosym ?
				 "%!n" : "%n",
				 format_arg[spec->arg_num].a.a_double) < 0) {
			goto error;
		    }

		    str = fmtbuf;
		    goto insertstr;

		case L's':
		    // Insert a string (const char * or const wchar_t *)
		    if (spec->flag_long) {
			str = format_arg[spec->arg_num].a.a_wstring;
		    } else {
			const char *p = format_arg[spec->arg_num].a.a_string;
			if (p == NULL) {
			    str = NULL;
			} else {
			    xmbstowcs(fmtbuf, p, BUFSIZE);
			    str = fmtbuf;
			}
		    }

		    if (str == NULL) {
			str = L"(NULL)";	// As per GNU printf()
		    }

		insertstr:
		    // Insert the string pointed to by str
		    while (*str != L'\0' && count > 1 && line < maxlines) {
			if (mkchstr_add(&outbuf, &attrbuf, &count, curattr,
					maxlines, maxwidth, &line, &width,
					&lastspc, &spcattr, &widthspc,
					widthbuf, widthbufsize, &str) < 0) {
			    goto error;
			}
		    }

		    wcformat += spec->len;
		    spec++;
		    break;

		default:
		    assert(spec->spec);
		}
	    }
	    break;

	default:
	    // Process an ordinary character (including new-line)
	    if (mkchstr_add(&outbuf, &attrbuf, &count, curattr, maxlines,
			    maxwidth, &line, &width, &lastspc, &spcattr,
			    &widthspc, widthbuf, widthbufsize, &wcformat) < 0) {
		goto error;
	    }
	}
    }

    *outbuf = L'\0';			// Terminating NUL character
    *attrbuf = 0;

    if (line >= 0 && line < maxlines) {
	widthbuf[line] = width;
    } else if (line >= maxlines) {
	line = maxlines - 1;
    }

    // Convert the (outbuf, attrbuf) pair of arrays to chbuf
    mkchstr_conv(chbuf, chbufsize, orig_outbuf, orig_attrbuf);

    free(fmtbuf);
    free(orig_wcformat);
    free(orig_attrbuf);
    free(orig_outbuf);

    return line + 1;


error_inval:
    errno = EINVAL;

error:
    saved_errno = errno;
    free(fmtbuf);
    free(orig_wcformat);
    free(orig_attrbuf);
    free(orig_outbuf);
    errno = saved_errno;

    errno_exit(_("mkchstr: '%s'"), format);
}


/***********************************************************************/
// leftch: Print strings in chstr left-aligned

int leftch (WINDOW *win, int y, int x, const chtype *restrict chstr,
	    int lines, const int *restrict widthbuf)
{
    assert(win != NULL);
    assert(chstr != NULL);
    assert(lines > 0);
    assert(widthbuf != NULL);

    wmove(win, y, x);
    for ( ; *chstr != 0; chstr++) {
	if (*chstr == '\n') {
	    wmove(win, getcury(win) + 1, x);
	} else {
	    waddch(win, *chstr);
	}
    }

    return OK;
}


/***********************************************************************/
// centerch: Print strings in chstr centred in window

int centerch (WINDOW *win, int y, int offset, const chtype *restrict chstr,
	      int lines, const int *restrict widthbuf)
{
    int ln = 0;


    assert(win != NULL);
    assert(chstr != NULL);
    assert(lines > 0);
    assert(widthbuf != NULL);

    wmove(win, y, (getmaxx(win) - widthbuf[ln]) / 2 + offset);
    for ( ; *chstr != 0; chstr++) {
	if (*chstr == '\n') {
	    if (ln++ >= lines) {
		return ERR;
	    } else {
		wmove(win, getcury(win) + 1,
		      (getmaxx(win) - widthbuf[ln]) / 2 + offset);
	    }
	} else {
	    waddch(win, *chstr);
	}
    }

    return OK;
}


/***********************************************************************/
// rightch: Print strings in chstr right-aligned

int rightch (WINDOW *win, int y, int x, const chtype *restrict chstr,
	     int lines, const int *restrict widthbuf)
{
    int ln = 0;


    assert(win != NULL);
    assert(chstr != NULL);
    assert(lines > 0);
    assert(widthbuf != NULL);

    wmove(win, y, x - widthbuf[ln]);
    for ( ; *chstr != 0; chstr++) {
	if (*chstr == '\n') {
	    if (ln++ >= lines) {
		return ERR;
	    } else {
		wmove(win, getcury(win) + 1, x - widthbuf[ln]);
	    }
	} else {
	    waddch(win, *chstr);
	}
    }

    return OK;
}


/***********************************************************************/
// left: Print strings left-aligned

int left (WINDOW *win, int y, int x, chtype attr_norm, chtype attr_alt1,
	  chtype attr_alt2, int maxlines, const char *restrict format, ...)
{
    va_list args;
    chtype *chbuf;
    int *widthbuf;
    int lines;
    int ret;


    assert(maxlines > 0);

    chbuf = xmalloc(BUFSIZE * sizeof(chtype));
    widthbuf = xmalloc(maxlines * sizeof(int));

    va_start(args, format);
    lines = vmkchstr(chbuf, BUFSIZE, attr_norm, attr_alt1, attr_alt2, maxlines,
		     getmaxx(win) - x - 2, widthbuf, maxlines, format, args);
    ret = leftch(win, y, x, chbuf, lines, widthbuf);
    assert(ret == OK);
    va_end(args);

    free(widthbuf);
    free(chbuf);
    return lines;
}


/***********************************************************************/
// center: Print strings centred in window

int center (WINDOW *win, int y, int offset, chtype attr_norm, chtype attr_alt1,
	    chtype attr_alt2, int maxlines, const char *restrict format, ...)
{
    va_list args;
    chtype *chbuf;
    int *widthbuf;
    int lines;
    int ret;


    assert(maxlines > 0);

    chbuf = xmalloc(BUFSIZE * sizeof(chtype));
    widthbuf = xmalloc(maxlines * sizeof(int));

    va_start(args, format);
    lines = vmkchstr(chbuf, BUFSIZE, attr_norm, attr_alt1, attr_alt2, maxlines,
		     getmaxx(win) - 4, widthbuf, maxlines, format, args);
    ret = centerch(win, y, offset, chbuf, lines, widthbuf);
    assert(ret == OK);
    va_end(args);

    free(widthbuf);
    free(chbuf);
    return lines;
}


/***********************************************************************/
// right: Print strings right-aligned

int right (WINDOW *win, int y, int x, chtype attr_norm, chtype attr_alt1,
	   chtype attr_alt2, int maxlines, const char *restrict format, ...)
{
    va_list args;
    chtype *chbuf;
    int *widthbuf;
    int lines;
    int ret;


    assert(maxlines > 0);

    chbuf = xmalloc(BUFSIZE * sizeof(chtype));
    widthbuf = xmalloc(maxlines * sizeof(int));

    va_start(args, format);
    lines = vmkchstr(chbuf, BUFSIZE, attr_norm, attr_alt1, attr_alt2, maxlines,
		     x - 2, widthbuf, maxlines, format, args);
    ret = rightch(win, y, x, chbuf, lines, widthbuf);
    assert(ret == OK);
    va_end(args);

    free(widthbuf);
    free(chbuf);
    return lines;
}


/***********************************************************************/
// getwch: Get a wide character from the keyboard

/* There are two implementations of getwch(): one used if enhanced Curses
   is present (with wide-character functions), the other if only
   single-byte functions are available. */

#if defined(HAVE_CURSES_ENHANCED) || defined(HAVE_NCURSESW)

int getwch (WINDOW *win, wint_t *restrict wch)
{
    int ret = wget_wch(win, wch);

    if (ret == OK) {
	int c = wctob(*wch);
	if ((c >= 0 && c < ' ') || c == 0x7F) {
	    /* Make control characters (and DEL) appear to be similar to
	       function keys.  This assumes the KEY_xxx definitions do
	       not overlap, which is true of all Curses implementations
	       (due to the same codes being returned by getch()).  We do
	       not use iswcntrl() as certain additional Unicode
	       characters are also control characters (eg, U+2028) */
	    *wch = (wint_t) c;
	    ret = KEY_CODE_YES;
	}
    }

    return ret;
}

#else // !defined(HAVE_CURSES_ENHANCED) && !defined(HAVE_NCURSESW)

int getwch (WINDOW *win, wint_t *restrict wch)
{
    static mbstate_t *mbstate;		// Current shift state

    char buf[MB_LEN_MAX * 8];		// Allow space for redundant shifts
    mbstate_t mbcopy;
    int len, ret;
    wchar_t val = 0;


    if (mbstate == NULL) {
	mbstate = xmalloc(sizeof(mbstate_t));
	memset(mbstate, 0, sizeof(mbstate_t));
    }

    len = 0;
    while (true) {
	ret = wgetch(win);
	if (ret == ERR) {
	    break;
	} else if (ret >= 0400) {
	    // Assume any non-ASCII result is a function key
	    if (len > 0) {
		// Function key is interrupting an incomplete multibyte char!
		ungetch(ret);
		ret = ERR;
	    } else {
		val = ret;
		ret = KEY_CODE_YES;
	    }
	    break;
	} else if (len >= sizeof(buf) - 1) {
	    // Too many characters
	    ungetch(ret);
	    ret = ERR;
	    break;
	} else {
	    buf[len++] = ret;

	    // Do we have a complete multibyte keypress?
	    memcpy(&mbcopy, mbstate, sizeof(mbstate_t));
	    size_t n = mbrtowc(&val, buf, len, &mbcopy);
	    if (n == (size_t) -1) {
		// Invalid character
		ungetch(ret);
		ret = ERR;
		break;
	    } else if (n == (size_t) -2) {
		// Incomplete sequence: wait for more bytes to come
		;
	    } else {
		// A valid multibyte sequence
		memcpy(mbstate, &mbcopy, sizeof(mbstate_t));

		int c = wctob(val);
		if ((c >= 0 && c < ' ') || c == 0x7F) {
		    /* Make control characters (and DEL) appear to be
		       similar to function keys. */
		    val = (wchar_t) c;
		    ret = KEY_CODE_YES;
		} else {
		    // An ordinary key
		    ret = OK;
		}
		break;
	    }
	}
    }

    if (wch != NULL) {
	*wch = val;
    }
    return ret;
}

#endif // !defined(HAVE_CURSES_ENHANCED) && !defined(HAVE_NCURSESW)


/***********************************************************************/
// gettxchar: Read a character from the keyboard

int gettxchar (WINDOW *win, wint_t *restrict wch)
{
    int ret;


    assert(win != NULL);
    assert(wch != NULL);

    keypad(win, true);
    meta(win, true);
    wtimeout(win, -1);

    while (true) {
	ret = getwch(win, wch);
	if (ret == OK) {
	    break;
	} else if (ret == KEY_CODE_YES) {

#ifdef HANDLE_RESIZE_EVENTS
	    if (*wch == KEY_RESIZE) {
		txresize();
	    } else {
		break;
	    }
#else // ! HANDLE_RESIZE_EVENTS
	    break;
#endif // ! HANDLE_RESIZE_EVENTS

	} else {
	    // ret == ERR
	    beep();
	}
    }

    return ret;
}


/***********************************************************************/
// cpos_end: Adjust cpos and st for printing the ending part of buf

void cpos_end (const wchar_t *restrict buf, int *restrict cpos,
	       int *restrict st, int clen, int width, int len)
{
    *cpos = MIN(clen, width - 1);

    if (clen <= width - 1) {
	// Whole string can be displayed
	*st = 0;
    } else {
	// String too long: figure out offset from which to print (value of st)
	int i = width - 1;
	*st = len;
	while (i > 0) {
	    i -= wcwidth(buf[--(*st)]);
	}
	if (i < 0) {
	    /* Don't truncate a double-width character if the second half
	       would appear in the first column position. */
	    i += wcwidth(buf[(*st)++]);
	    while (wcwidth(buf[*st]) == 0) {
		// Skip over zero-width characters (mostly combining ones)
		(*st)++;
	    }
	    *cpos -= i;
	}
    }

    assert(*st >= 0);
}


/***********************************************************************/
// cpos_dec: Adjust cpos and st: scroll to the left by w columns

void cpos_dec (const wchar_t *restrict buf, int *restrict cpos,
	       int *restrict st, int w, int width __attribute__((unused)))
{
    if (*cpos - w >= 0) {
	// Cursor position is not yet in first column
	*cpos -= w;
    } else if (*st > 0) {
	(*st)--;
	if (w == 0) {
	    /* Make sure zero-width characters (esp. combining ones) do
	       not appear without the associated base character */
	    w = wcwidth(buf[*st]);
	    while (*st > 0 && w == 0) {
		w = wcwidth(buf[--(*st)]);
	    }
	    *cpos = w;
	}
    }
}


/***********************************************************************/
// cpos_inc: Adjust cpos and st: scroll to the right by w columns

void cpos_inc (const wchar_t *restrict buf, int *restrict cpos,
	       int *restrict st, int w, int width)
{
    if (*cpos + w <= width - 1) {
	// Cursor position is not yet in second-last column
	*cpos += w;
    } else {
	int i = 0;
	while (i < w) {
	    i += wcwidth(buf[(*st)++]);
	}
	while (wcwidth(buf[*st]) == 0) {
	    // Skip over zero-width characters (mainly combining ones)
	    (*st)++;
	}
	if (i > w) {
	    // Take double-width characters into account
	    *cpos -= i - w;
	}
    }
}


/***********************************************************************/
// gettxline: Read a line of input from the keyboard (low-level)

int gettxline (WINDOW *win, wchar_t *restrict buf, int bufsize,
	       bool *restrict modified, bool multifield,
	       const wchar_t *emptyval, const wchar_t *defaultval,
	       const wchar_t *allowed, bool stripspc, int y, int x,
	       int width, chtype attr)
{
    wchar_t *keycode_defval;
    bool done, redraw, mod;
    int len, pos, st;
    int clen, cpos;
    int rcode, ret;
    wint_t key;
    chtype oldattr;
    chtype *chbuf;
    int chbufwidth;


    assert(win != NULL);
    assert(buf != NULL);
    assert(bufsize > 2);
    assert(width > 2);

    chbuf = xmalloc(BUFSIZE * sizeof(chtype));

    keycode_defval = xmalloc(BUFSIZE * sizeof(wchar_t));
    /* TRANSLATORS: This string specifies the keycodes used to insert the
       default value into the input string, if entered as the very first
       character.  Ideally, it should contain an easily-accessible
       keycode that would NOT be used in ordinary input.  Digits, ".",
       ",", "+" and "-" are definitely NOT acceptable. */
    xmbstowcs(keycode_defval, pgettext("input|DefaultValue", "=;"), BUFSIZE);

    keypad(win, true);
    meta(win, true);
    wtimeout(win, -1);

    oldattr = getattrs(win);
    curs_set(CURS_ON);

    len = wcslen(buf);			// len is number of wide chars in buf
    pos = len;				// pos (string position): 0 to len
    clen = wcswidth(buf, bufsize);	// clen is number of column positions

    if (clen < 0) {
	err_exit(_("gettxline: illegal character in string: '%ls'"), buf);
    }

    /* Find the point from which buf should be displayed to screen.  cpos
       is the cursor position (0 to width-1), st is the offset in buf[]
       from which to start printing the string on the screen. */
    cpos_end(buf, &cpos, &st, clen, width, len);

    redraw = true;
    done = false;
    mod = false;
    ret = OK;

    while (! done) {
	if (redraw) {
	    /* Redisplay the visible part of the current input string.
	       Blanks at the end of the input area are replaced with
	       "attr", which may contain a '_' for non-colour mode. */
	    mvwhline(win, y, x, ((attr & A_CHARTEXT) == 0) ?
		     ' ' | attr : attr, width);
	    mkchstr(chbuf, BUFSIZE, attr & ~A_CHARTEXT, 0, 0, 1, width,
		    &chbufwidth, 1, "%ls", buf + st);
	    leftch(win, y, x, chbuf, 1, &chbufwidth);

	    wmove(win, y, x + cpos);
	    wrefresh(win);
	}

	rcode = getwch(win, &key);

	if (rcode == OK) {
	    // Ordinary wide character

	    if (defaultval != NULL && len == 0
		&& wcschr(keycode_defval, key) != NULL) {
		// Initialise buffer with the default value

		wcsncpy(buf, defaultval, bufsize - 1);
		buf[bufsize - 1] = L'\0';

		len = wcslen(buf);
		pos = len;
		clen = wcswidth(buf, bufsize);

		if (clen == -1) {
		    err_exit(_("gettxline: illegal character in string: '%ls'"),
			     buf);
		}

		cpos_end(buf, &cpos, &st, clen, width, len);

		mod = true;
		redraw = true;

	    } else if (len >= bufsize - 1
		       || (allowed != NULL && wcschr(allowed, key) == NULL)) {
		beep();

	    } else {
		// Process an ordinary character: insert it into the string
		int w = wcwidth(key);

		if (w < 0) {
		    // Non-printing character
		    beep();

		} else {
		    wmemmove(buf + pos + 1, buf + pos, len - pos + 1);
		    buf[pos] = (wchar_t) key;
		    len++;
		    pos++;

		    clen += w;
		    cpos_inc(buf, &cpos, &st, w, width);

		    mod = true;
		    redraw = true;
		}
	    }

	} else if (rcode == KEY_CODE_YES) {
	    // Function or control key
	    switch (key) {

	    // Terminating keys

	    case KEY_RETURN:
	    case KEY_ENTER:
	    case KEY_CTRL('M'):
		// Finish entering the string

		if (stripspc) {
		    int i;

		    // Strip leading spaces
		    for (i = 0; i < len && iswspace(buf[i]); i++)
			;
		    if (i > 0) {
			wmemmove(buf, buf + i, len - i + 1);
			len -= i;
			mod = true;
		    }

		    // Strip trailing spaces
		    for (i = len; i > 0 && iswspace(buf[i - 1]); i--)
			;
		    if (i < len) {
			buf[i] = L'\0';
			len = i;
			mod = true;
		    }
		}

		if (emptyval != NULL && len == 0) {
		    wcsncpy(buf, emptyval, bufsize - 1);
		    buf[bufsize - 1] = L'\0';
		    mod = true;
		}

		ret = OK;
		done = true;
		break;

	    case KEY_CANCEL:
	    case KEY_EXIT:
	    case KEY_CTRL('C'):
	    case KEY_CTRL('G'):
	    case KEY_CTRL('\\'):
		// Cancel entering the string
		ret = ERR;
		done = true;
		break;

	    case KEY_UP:
	    case KEY_BTAB:
	    case KEY_CTRL('P'):
		// Finish entering the string, if multifield is true
		if (multifield) {
		    ret = KEY_UP;
		    done = true;
		} else {
		    beep();
		}
		break;

	    case KEY_DOWN:
	    case KEY_TAB:
	    case KEY_CTRL('N'):
		// Finish entering the string, if multifield is true
		if (multifield) {
		    ret = KEY_DOWN;
		    done = true;
		} else {
		    beep();
		}
		break;

	    // Cursor movement keys

	    case KEY_LEFT:
	    case KEY_CTRL('B'):
		// Move cursor back one character
		if (pos == 0) {
		    beep();
		} else {
		    pos--;
		    cpos_dec(buf, &cpos, &st, wcwidth(buf[pos]), width);
		    redraw = true;
		}
		break;

	    case KEY_RIGHT:
	    case KEY_CTRL('F'):
		// Move cursor forward one character
		if (pos == len) {
		    beep();
		} else {
		    pos++;
		    cpos_inc(buf, &cpos, &st, wcwidth(buf[pos - 1]), width);
		    redraw = true;
		}
		break;

	    case KEY_HOME:
	    case KEY_CTRL('A'):
		// Move cursor to start of string
		pos = 0;
		cpos = 0;
		st = 0;
		redraw = true;
		break;

	    case KEY_END:
	    case KEY_CTRL('E'):
		// Move cursor to end of string
		pos = len;
		cpos_end(buf, &cpos, &st, clen, width, len);
		redraw = true;
		break;

	    case KEY_CLEFT:
		// Move cursor to start of current or previous word
		while (pos > 0 && ! iswalnum(buf[pos - 1])) {
		    pos--;
		    cpos_dec(buf, &cpos, &st, wcwidth(buf[pos]), width);
		}
		while (pos > 0 && (iswalnum(buf[pos - 1])
		       || (pos > 1 && wcwidth(buf[pos - 1]) == 0
			   && iswalnum(buf[pos - 2])))) {
		    /* Treat zero-width characters preceded by an
		       alphanumeric character as alphanumeric. */
		    pos--;
		    cpos_dec(buf, &cpos, &st, wcwidth(buf[pos]), width);
		}
		redraw = true;
		break;

	    case KEY_CRIGHT:
		// Move cursor to end of current or next word
		while (pos < len && ! iswalnum(buf[pos])) {
		    pos++;
		    cpos_inc(buf, &cpos, &st, wcwidth(buf[pos - 1]), width);
		}
		while (pos < len
		       && (iswalnum(buf[pos]) || wcwidth(buf[pos]) == 0)) {
		    /* Treat zero-width characters following an
		       alphanumeric character as alphanumeric. */
		    pos++;
		    cpos_inc(buf, &cpos, &st, wcwidth(buf[pos - 1]), width);
		}
		redraw = true;
		break;

	    // Deletion keys

	    case KEY_BS:
	    case KEY_BACKSPACE:
	    case KEY_DEL:
		// Delete previous character
		if (pos == 0) {
		    beep();
		} else {
		    int w = wcwidth(buf[pos - 1]);
		    wmemmove(buf + pos - 1, buf + pos, len - pos + 1);
		    len--;
		    pos--;
		    clen -= w;
		    cpos_dec(buf, &cpos, &st, w, width);
		    mod = true;
		    redraw = true;
		}
		break;

	    case KEY_DC:
	    case KEY_CTRL('D'):
		// Delete character under cursor
		if (pos == len) {
		    beep();
		} else {
		    int w = wcwidth(buf[pos]);
		    wmemmove(buf + pos, buf + pos + 1, len - pos);
		    len--;
		    clen -= w;
		    // pos, cpos and st stay the same
		    mod = true;
		    redraw = true;
		}
		break;

	    case KEY_CLEAR:
		// Delete the entire line
		wcscpy(buf, L"");
		len = 0;
		pos = 0;
		clen = 0;
		cpos = 0;
		st = 0;
		mod = true;
		redraw = true;
		break;

	    case KEY_CTRL('U'):
		// Delete backwards to the start of the line
		if (pos == 0) {
		    beep();
		} else {
		    int i, ww;
		    for (i = 0, ww = 0; i < pos; i++) {
			ww += wcwidth(buf[i]);
		    }

		    wmemmove(buf, buf + pos, len - pos + 1);
		    len -= pos;
		    pos = 0;
		    clen -= ww;
		    cpos = 0;
		    st = 0;
		    mod = true;
		    redraw = true;
		}
		break;

	    case KEY_CTRL('K'):
		// Delete to the end of the line
		if (pos == len) {
		    beep();
		} else {
		    int i, ww;
		    for (i = pos, ww = 0; i < len; i++) {
			ww += wcwidth(buf[i]);
		    }

		    buf[pos] = L'\0';
		    len = pos;
		    clen -= ww;
		    // pos, cpos and st stay the same
		    mod = true;
		    redraw = true;
		}
		break;

	    case KEY_CTRL('W'):
		// Delete the previous word
		if (pos == 0) {
		    beep();
		} else {
		    /* Note the use of iswspace() instead of iswalnum():
		       this makes ^W follow GNU Bash standards, which
		       behaves differently from Meta-DEL. */
		    int i = pos;
		    int ww = 0;
		    while (i > 0 && iswspace(buf[i - 1])) {
			i--;
			int w = wcwidth(buf[i]);
			ww += w;
			cpos_dec(buf, &cpos, &st, w, width);
		    }
		    while (i > 0 && ! iswspace(buf[i - 1])) {
			i--;
			int w = wcwidth(buf[i]);
			ww += w;
			cpos_dec(buf, &cpos, &st, w, width);
		    }

		    wmemmove(buf + i, buf + pos, len - pos + 1);
		    len -= pos - i;
		    pos = i;
		    clen -= ww;
		    mod = true;
		    redraw = true;
		}
		break;

	    // Miscellaneous keys and events

	    case KEY_CTRL('T'):
		// Transpose characters
		if (pos == 0 || len <= 1) {
		    beep();
		} else if (pos == len) {
		    wchar_t c = buf[pos - 1];
		    buf[pos - 1] = buf[pos - 2];
		    buf[pos - 2] = c;

		    // pos, cpos and st stay the same
		    mod = true;
		    redraw = true;
		} else {
		    wchar_t c = buf[pos];
		    int w = wcwidth(c);
		    buf[pos] = buf[pos - 1];
		    buf[pos - 1] = c;

		    pos++;
		    cpos_inc(buf, &cpos, &st, w, width);
		    mod = true;
		    redraw = true;
		}
		break;

	    case KEY_ESC:
		// Handle Meta-X-style and other function key presses
		wtimeout(win, META_TIMEOUT);
		rcode = getwch(win, &key);

		if (rcode == OK) {
		    // Ordinary wide character

		    // Swallow any unknown VT100-style function keys
		    if (key == L'O' || key == L'[') {
			rcode = getwch(win, &key);
			while (rcode == OK
			       && wcschr(L"0123456789;", key) != NULL
			       && wcschr(L"~ABCDEFGHIJKLMNOPQRSTUVWXYZ"
					 L"abcdefghijklmnopqrstuvwxyz", key)
			       == NULL) {
			    rcode = getwch(win, &key);
			}
			beep();

		    } else {
			// Handle Meta-X-style keypress
			switch (key) {

			// Cursor movement keys

			case L'B':
			case L'b':
			    // Move cursor to start of current or previous word
			    while (pos > 0 && ! iswalnum(buf[pos - 1])) {
				pos--;
				cpos_dec(buf, &cpos, &st, wcwidth(buf[pos]),
					 width);
			    }
			    while (pos > 0 && (iswalnum(buf[pos - 1])
				   || (pos > 1 && wcwidth(buf[pos - 1]) == 0
				       && iswalnum(buf[pos - 2])))) {
				/* Treat zero-width characters preceded by an
				   alphanumeric character as alphanumeric. */
				pos--;
				cpos_dec(buf, &cpos, &st, wcwidth(buf[pos]),
					 width);
			    }
			    redraw = true;
			    break;

			case L'F':
			case L'f':
			    // Move cursor to end of current or next word
			    while (pos < len && ! iswalnum(buf[pos])) {
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    while (pos < len && (iswalnum(buf[pos])
				   || wcwidth(buf[pos]) == 0)) {
				/* Treat zero-width characters following an
				   alphanumeric character as alphanumeric. */
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    redraw = true;
			    break;

			// Deletion keys

			case L'D':
			case L'd':
			    // Delete the next word
			    {
				int i = pos;
				int ww = 0;
				while (i < len && ! iswalnum(buf[i])) {
				    i++;
				    ww += wcwidth(buf[i - 1]);
				}
				while (i < len && (iswalnum(buf[i])
				       || wcwidth(buf[pos]) == 0)) {
				    /* Treat zero-width characters following
				       an alphanumeric character as
				       alphanumeric. */
				    i++;
				    ww += wcwidth(buf[i - 1]);
				}

				wmemmove(buf + pos, buf + i, len - i + 1);
				len -= (i - pos);
				clen -= ww;
				// pos, cpos and st stay the same
				mod = true;
				redraw = true;
			    }
			    break;

			case L'\\':
			case L' ':
			    // Delete all surrounding spaces; if key == L' ',
			    // also insert one space
			    {
				int i = pos;
				int ww = 0;
				while (pos > 0 && iswspace(buf[pos - 1])) {
				    pos--;
				    int w = wcwidth(buf[pos]);
				    ww += w;
				    cpos_dec(buf, &cpos, &st, w, width);
				}
				while (i < len && iswspace(buf[i])) {
				    i++;
				    ww += wcwidth(buf[i - 1]);
				}

				wmemmove(buf + pos, buf + i, len - i + 1);
				len -= (i - pos);
				clen -= ww;

				if (key == L' ') {
				    if (len >= bufsize - 1 || (allowed != NULL
					&& wcschr(allowed, key) == NULL)) {
					beep();
				    } else {
					wchar_t c = L' ';
					wmemmove(buf + pos + 1, buf + pos,
						 len - pos + 1);
					buf[pos] = c;
					len++;
					pos++;

					int w = wcwidth(c);
					clen += w;
					cpos_inc(buf, &cpos, &st, w, width);
				    }
				}

				mod = true;
				redraw = true;
			    }
			    break;

			// Transformation keys

			case L'U':
			case L'u':
			    // Convert word (from cursor onwards) to upper case
			    while (pos < len && ! iswalnum(buf[pos])) {
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    while (pos < len && (iswalnum(buf[pos])
				   || wcwidth(buf[pos]) == 0)) {
				buf[pos] = towupper(buf[pos]);
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    mod = true;
			    redraw = true;
			    break;

			case L'L':
			case L'l':
			    // Convert word (from cursor onwards) to lower case
			    while (pos < len && ! iswalnum(buf[pos])) {
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    while (pos < len && (iswalnum(buf[pos])
				   || wcwidth(buf[pos]) == 0)) {
				buf[pos] = towlower(buf[pos]);
				pos++;
				cpos_inc(buf, &cpos, &st,
					 wcwidth(buf[pos - 1]), width);
			    }
			    mod = true;
			    redraw = true;
			    break;

			case L'C':
			case L'c':
			    // Convert current letter to upper case,
			    // following letters to lower case
			    {
				while (pos < len && ! iswalnum(buf[pos])) {
				    pos++;
				    cpos_inc(buf, &cpos, &st,
					     wcwidth(buf[pos - 1]), width);
				}
				bool first = true;
				while (pos < len && (iswalnum(buf[pos])
				       || wcwidth(buf[pos]) == 0)) {
				    if (first) {
					buf[pos] = towupper(buf[pos]);
					first = false;
				    } else {
					buf[pos] = towlower(buf[pos]);
				    }
				    pos++;
				    cpos_inc(buf, &cpos, &st,
					     wcwidth(buf[pos - 1]), width);
				}
				mod = true;
				redraw = true;
			    }
			    break;

			default:
			    beep();
			}
		    }

		} else if (rcode == KEY_CODE_YES) {
		    // Function or control key (with preceding Meta key)

		    switch (key) {

		    // Deletion keys

		    case KEY_BS:
		    case KEY_BACKSPACE:
		    case KEY_DEL:
			// Delete the previous word (different from ^W)
			{
			    int ww = 0;
			    int i = pos;
			    while (i > 0 && ! iswalnum(buf[i - 1])) {
				i--;
				int w = wcwidth(buf[i]);
				ww += w;
				cpos_dec(buf, &cpos, &st, w, width);
			    }
			    while (i > 0 && (iswalnum(buf[i - 1])
				   || (i > 1 && wcwidth(buf[i - 1]) == 0
				       && iswalnum(buf[i - 2])))) {
				/* Treat zero-width characters preceded by an
				   alphanumeric character as alphanumeric. */
				i--;
				int w = wcwidth(buf[i]);
				ww += w;
				cpos_dec(buf, &cpos, &st, w, width);
			    }

			    wmemmove(buf + i, buf + pos, len - pos + 1);
			    len -= (pos - i);
			    pos = i;
			    clen -= ww;
			    mod = true;
			    redraw = true;
			}
			break;

		    // Miscellaneous keys and events

#ifdef HANDLE_RESIZE_EVENTS
		    case KEY_RESIZE:
			txresize();
			break;
#endif // HANDLE_RESIZE_EVENTS

		    default:
			beep();
		    }

		} else {
		    /* rcode == ERR (timeout): <ESC> by itself, so cancel
		       entering the string. */
		    ret = ERR;
		    done = true;
		}

		wtimeout(win, -1);
		break;

#ifdef HANDLE_RESIZE_EVENTS
	    case KEY_RESIZE:
		txresize();
		break;
#endif // HANDLE_RESIZE_EVENTS

	    default:
		beep();
	    }
	} else {
	    // rcode == ERR: Do nothing
	    ;
	}
    }

    curs_set(CURS_OFF);

    mvwhline(win, y, x, ' ' | oldattr, width);
    mkchstr(chbuf, BUFSIZE, oldattr | A_BOLD, 0, 0, 1, width,
	    &chbufwidth, 1, "%ls", buf);
    leftch(win, y, x, chbuf, 1, &chbufwidth);
    wrefresh(win);

    if (modified != NULL) {
	*modified = mod;
    }

    free(chbuf);
    free(keycode_defval);
    return ret;
}


/***********************************************************************/
// gettxstr: Read a string from the keyboard

int gettxstr (WINDOW *win, wchar_t *restrict *restrict bufptr,
	      bool *restrict modified, bool multifield, int y, int x,
	      int width, chtype attr)
{
    assert(bufptr != NULL);


    // Allocate the result buffer if needed
    if (*bufptr == NULL) {
	*bufptr = xmalloc(BUFSIZE * sizeof(wchar_t));
	**bufptr = L'\0';
    }

    return gettxline(win, *bufptr, BUFSIZE, modified, multifield, L"", L"",
		     NULL, true, y, x, width, attr);
}


/***********************************************************************/
// txinput_fixup: Copy strings with fixup

void txinput_fixup (wchar_t *restrict dest, const wchar_t *restrict src,
		    bool isfloat)
{
    assert(src != NULL);
    assert(dest != NULL);

    wcsncpy(dest, src, BUFSIZE - 1);
    dest[BUFSIZE - 1] = L'\0';

    // Replace mon_decimal_point with decimal_point if these are different
    if (isfloat) {
	if (*mon_decimal_point != L'\0' && *decimal_point != L'\0'
	    && wcscmp(mon_decimal_point, decimal_point) != 0) {

	    int len_dp  = wcslen(decimal_point);
	    int len_mdp = wcslen(mon_decimal_point);
	    wchar_t *p;

	    while ((p = wcsstr(dest, mon_decimal_point)) != NULL) {
		// Make space for decimal_point, if needed
		if (len_mdp != len_dp) {
		    wmemmove(p + len_dp, p + len_mdp,
			     wcslen(p) - (len_dp - len_mdp) + 1);
		}

		// Copy decimal_point over p WITHOUT copying ending NUL
		for (wchar_t *pn = decimal_point; *pn != L'\0'; pn++, p++) {
		    *p = *pn;
		}
	    }
	}
    }

    // Remove thousands separators if required
    if (*thousands_sep != L'\0') {
	int len = wcslen(thousands_sep);
	wchar_t *p;

	while ((p = wcsstr(dest, thousands_sep)) != NULL) {
	    wmemmove(p, p + len, wcslen(p) - len + 1);
	}
    }
    if (*mon_thousands_sep != L'\0') {
	int len = wcslen(mon_thousands_sep);
	wchar_t *p;

	while ((p = wcsstr(dest, mon_thousands_sep)) != NULL) {
	    wmemmove(p, p + len, wcslen(p) - len + 1);
	}
    }
}


/***********************************************************************/
// gettxdouble: Read a floating-point number from the keyboard

int gettxdouble (WINDOW *win, double *restrict result, double min,
		 double max, double emptyval, double defaultval,
		 int y, int x, int width, chtype attr)
{
    struct lconv *lc = &lconvinfo;

    wchar_t *buf, *bufcopy;
    wchar_t *allowed, *emptystr, *defaultstr;
    double val;
    bool done;
    int ret;


    assert(result != NULL);
    assert(min <= max);

    buf        = xmalloc(BUFSIZE * sizeof(wchar_t));
    bufcopy    = xmalloc(BUFSIZE * sizeof(wchar_t));
    allowed    = xmalloc(BUFSIZE * sizeof(wchar_t));
    emptystr   = xmalloc(BUFSIZE * sizeof(wchar_t));
    defaultstr = xmalloc(BUFSIZE * sizeof(wchar_t));

    *buf = L'\0';

    wcscpy(allowed,  L"0123456789+-Ee");
    wcsncat(allowed, decimal_point,     BUFSIZE - wcslen(allowed) - 1);
    wcsncat(allowed, thousands_sep,     BUFSIZE - wcslen(allowed) - 1);
    wcsncat(allowed, mon_decimal_point, BUFSIZE - wcslen(allowed) - 1);
    wcsncat(allowed, mon_thousands_sep, BUFSIZE - wcslen(allowed) - 1);

    swprintf(emptystr,   BUFSIZE, L"%'1.*f", lc->frac_digits, emptyval);
    swprintf(defaultstr, BUFSIZE, L"%'1.*f", lc->frac_digits, defaultval);

    done = false;
    while (! done) {
	ret = gettxline(win, buf, BUFSIZE, NULL, false, emptystr, defaultstr,
			allowed, true, y, x, width, attr);

	if (ret == OK) {
	    wchar_t *p;

	    txinput_fixup(bufcopy, buf, true);
	    val = wcstod(bufcopy, &p);

	    if (*p == L'\0' && val >= min && val <= max) {
		*result = val;
		done = true;
	    } else {
		beep();
	    }
	} else {
	    done = true;
	}
    }

    free(defaultstr);
    free(emptystr);
    free(allowed);
    free(bufcopy);
    free(buf);

    return ret;
}


/***********************************************************************/
// gettxlong: Read an integer number from the keyboard

int gettxlong (WINDOW *win, long int *restrict result, long int min,
	       long int max, long int emptyval, long int defaultval,
	       int y, int x, int width, chtype attr)
{
    wchar_t *buf, *bufcopy;
    wchar_t *allowed, *emptystr, *defaultstr;
    long int val;
    bool done;
    int ret;


    assert(result != NULL);
    assert(min <= max);

    buf        = xmalloc(BUFSIZE * sizeof(wchar_t));
    bufcopy    = xmalloc(BUFSIZE * sizeof(wchar_t));
    allowed    = xmalloc(BUFSIZE * sizeof(wchar_t));
    emptystr   = xmalloc(BUFSIZE * sizeof(wchar_t));
    defaultstr = xmalloc(BUFSIZE * sizeof(wchar_t));

    *buf = L'\0';

    wcscpy(allowed, L"0123456789+-");
    wcsncat(allowed, thousands_sep,     BUFSIZE - wcslen(allowed) - 1);
    wcsncat(allowed, mon_thousands_sep, BUFSIZE - wcslen(allowed) - 1);

    swprintf(emptystr,   BUFSIZE, L"%'1ld", emptyval);
    swprintf(defaultstr, BUFSIZE, L"%'1ld", defaultval);

    done = false;
    while (! done) {
	ret = gettxline(win, buf, BUFSIZE, NULL, false, emptystr, defaultstr,
			allowed, true, y, x, width, attr);

	if (ret == OK) {
	    wchar_t *p;

	    txinput_fixup(bufcopy, buf, false);
	    val = wcstol(bufcopy, &p, 10);

	    if (*p == L'\0' && val >= min && val <= max) {
		*result = val;
		done = true;
	    } else {
		beep();
	    }
	} else {
	    done = true;
	}
    }

    free(defaultstr);
    free(emptystr);
    free(allowed);
    free(bufcopy);
    free(buf);

    return ret;
}


/***********************************************************************/
// answer_yesno: Wait for a Yes/No answer

bool answer_yesno (WINDOW *win)
{
    static wchar_t *keycode_yes;
    static wchar_t *keycode_no;

    bool ret;

    chtype oldattr = getattrs(win);
    chtype oldbkgd = getbkgd(win);


    if (keycode_yes == NULL) {
	wchar_t *buf = xmalloc(BUFSIZE * sizeof(wchar_t));

	/* TRANSLATORS: The strings with msgctxt "input|Yes" and
	   "input|No" contain the keycodes used to determine whether a
	   user is answering "Yes" or "No" in response to some question.
	   Both upper and lower-case versions should be present. */
	xmbstowcs(buf, pgettext("input|Yes", "Yy"), BUFSIZE);
	keycode_yes = xwcsdup(buf);
	xmbstowcs(buf, pgettext("input|No",  "Nn"), BUFSIZE);
	keycode_no = xwcsdup(buf);

	free(buf);
    }


    keypad(win, true);
    meta(win, true);
    wtimeout(win, -1);

    curs_set(CURS_ON);

    while (true) {
	wint_t key;
	int r = getwch(win, &key);

	if (r == OK) {
	    if (wcschr(keycode_yes, key) != NULL) {
		ret = true;
		break;
	    } else if (wcschr(keycode_no, key) != NULL) {
		ret = false;
		break;
	    } else {
		beep();
	    }
	} else if (r == KEY_CODE_YES) {

#ifdef HANDLE_RESIZE_EVENTS
	    if (key == KEY_RESIZE) {
		txresize();
	    } else {
		beep();
	    }
#else // ! HANDLE_RESIZE_EVENTS
	    beep();
#endif // ! HANDLE_RESIZE_EVENTS

	} else {
	    beep();
	}
    }

    curs_set(CURS_OFF);
    wattron(win, A_BOLD);

    if (ret) {
	/* TRANSLATORS: The strings "Yes" and "No" are printed as a
	   response to user input in answer to questions like "Are you
	   sure? [Y/N] " */
	waddstr(win, pgettext("answer", "Yes"));
    } else {
	waddstr(win, pgettext("answer", "No"));
    }

    wbkgdset(win, oldbkgd);
    wattrset(win, oldattr);

    wrefresh(win);
    return ret;
}


/***********************************************************************/
// wait_for_key: Print a message and wait for any key

void wait_for_key (WINDOW *win, int y, chtype attr)
{
    wint_t key;
    int r;


    keypad(win, true);
    meta(win, true);
    wtimeout(win, -1);

    center(curwin, y, 0, attr, 0, 0, 1,
	   /* TRANSLATORS: The reason the user is not asked "Press any
	      key to continue" is historical: many, many people used to
	      ask "where is the <ANY> key?" :-) */
	   _("[ Press <SPACE> to continue ] "));
    wrefresh(win);

    while (true) {
	r = getwch(win, &key);
	if (r == OK) {
	    break;
	} else if (r == KEY_CODE_YES) {

#ifdef HANDLE_RESIZE_EVENTS
	    if (key == KEY_RESIZE) {
		txresize();
	    } else {
		break;
	    }
#else // ! HANDLE_RESIZE_EVENTS
	    break;
#endif // ! HANDLE_RESIZE_EVENTS

	} else {
	    beep();
	}
    }
}


/***********************************************************************/
// End of file
