diff --git a/src/intf.c b/src/intf.c index 5bb0a70..9e11332 100644 --- a/src/intf.c +++ b/src/intf.c @@ -37,7 +37,7 @@ * Module constants and type declarations * ************************************************************************/ -#define OUTBUFSIZE (1024) /* Output string buffer size */ +#define BUFSIZE (1024) /* Size of various string buffers */ typedef struct txwin { @@ -314,13 +314,13 @@ int center (WINDOW *win, int y, const bool clrline, const char *format, ...) char *buf; - buf = malloc(OUTBUFSIZE); + buf = malloc(BUFSIZE); if (buf == NULL) { err_exit("out of memory"); } va_start(args, format); - len = vsnprintf(buf, OUTBUFSIZE, format, args); + len = vsnprintf(buf, BUFSIZE, format, args); va_end(args); if (len < 0) { return ERR; @@ -448,3 +448,660 @@ bool getanswer (WINDOW *win) wrefresh(win); return (key == 'Y'); } + + +/*----------------------------------------------------------------------- + Function: gettxline - Read a line from the keyboard (generic) + Arguments: win - Window to use + buf - Pointer to preallocated buffer + bufsize - Size of buffer + multifield - Allow , etc, to move between fields + maxlen - Maximum length of string + emptyval - String used if an empty string is input + defaultval - Default string, used if KEY_DEFAULTVAL is + pressed as the first character + allowed - Characters allowed in the string + stripspc - Strip leading and trailing spaces from string + y, x - Start of field (line, col) + fieldsize - Size of field in characters + attr - Curses attribute to use for the field + Returns: int - Status code: OK, ERR or key code + + This low-level function draws an input field fieldsize characters long + using attr as the window attribute, then reads a line of input from the + keyboard (of no more than maxlen characters) and places it into the + preallocated buffer buf[]. On entry, buf[] must contain the current + input string: this allows for resumed editing of an input line. On + exit, buf[] will contain the input string; this string is printed using + the original window attributes with A_BOLD added. Many EMACS/Bash- + style keyboard editing shortcuts are allowed. + + If ENTER (or equivalent) is pressed, OK is returned. In this case, + leading and trailing spaces are stripped if stripspc is true; if an + empty string is entered, the string pointed to by emptyval (if not + NULL) is stored in buf[]. + + If CANCEL, ESC or ^G is pressed, ERR is returned. In this case, buf[] + contains the string as edited by the user: emptyval is NOT used, nor + are leading and trailing spaces stripped. + + If multifield is true, the UP and DOWN arrow keys, as well as TAB, + Shift-TAB, ^P (Previous) and ^N (Next) return KEY_UP or KEY_DOWN as + appropriate. As with CANCEL etc., emptyval is NOT used, nor are + leading and trailing spaces stripped. + + If KEY_DEFAULTVAL is pressed when the input line is empty, the string + pointed to by defaultval (if not NULL) is placed in the buffer as if + typed by the user. Editing is NOT terminated in this case. + + If allowed points to a string (ie, is not NULL), only characters in + that string are allowed to be entered into the input line. For + example, if allowed points to "0123456789abcdefABCDEF", only those + characters would be allowed (allowing a hexadecimal number to be + entered). +*/ + +int gettxline (WINDOW *win, char *buf, int bufsize, bool multifield, + int maxlen, const char *emptyval, const char *defaultval, + const char *allowed, bool stripspc, int y, int x, + int fieldsize, int attr) +{ + int i, len, pos, cpos, nb; + int oldattr; + bool done, redraw, first; + char c; + int key, key2, ret; + + + assert(buf != NULL); + assert(bufsize > 1); + assert(maxlen > 0); + assert(maxlen < bufsize); + assert(fieldsize > 1); + + + keypad(win, true); + meta(win, true); + wtimeout(win, -1); + + oldattr = getbkgd(win) & ~A_CHARTEXT; + wattrset(win, attr & ~A_CHARTEXT); + curs_set(CURS_ON); + + len = strlen(buf); + pos = len; // pos can be from 0 to strlen(buf) + cpos = MIN(len, fieldsize - 1); + redraw = true; + done = false; + + 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. + */ + + mvwprintw(win, y, x, "%-*.*s", fieldsize, fieldsize, + buf + pos - cpos); + + nb = (len - (pos - cpos) > fieldsize) ? + 0 : fieldsize - (len - (pos - cpos)); + wmove(win, y, x + fieldsize - nb); + for (i = 0; i < nb; i++) { + waddch(win, ((attr & A_CHARTEXT) == 0) ? attr | ' ' : attr); + } + + wmove(win, y, x + cpos); + wrefresh(win); + } + + key = wgetch(win); + if (key == ERR) { + // Do nothing on ERR + ; + } else if ((key == KEY_DEFAULTVAL) && (defaultval != NULL) + && (len == 0)) { + // Initialise buffer with the default value + + strncpy(buf, defaultval, bufsize - 1); + buf[bufsize - 1] = '\0'; + len = strlen(buf); + pos = len; + cpos = MIN(len, fieldsize - 1); + redraw = true; + } else if ((key < 0400) && (! iscntrl(key))) { + if ((len >= maxlen) || + ((allowed != NULL) && (strchr(allowed, key) == NULL))) { + beep(); + } else { + // Process ordinary key press: insert it into the string + + memmove(buf + pos + 1, buf + pos, len - pos + 1); + buf[pos] = (char) key; + len++; + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + redraw = true; + } + } else { + switch (key) { + + // Terminating keys + + case KEY_RETURN: + case KEY_ENTER: + case KEY_CTRL('M'): + // Finish entering the string + + if (stripspc) { + // Strip leading spaces + i = 0; + while ((i < len) && isspace(buf[i])) { + i++; + } + memmove(buf, buf + i, len - i + 1); + len -= i; + + // Strip trailing spaces + i = len; + while ((i > 0) && isspace(buf[i - 1])) { + i--; + } + buf[i] = '\0'; + len = i; + } + + if ((emptyval != NULL) && (len == 0)) { + strncpy(buf, emptyval, bufsize - 1); + buf[bufsize - 1] = '\0'; + } + + ret = OK; + done = true; + break; + + case KEY_CANCEL: + case KEY_CTRL('G'): + // case KEY_CTRL('C'): + // 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--; + if (cpos > 0) { + cpos--; + } + redraw = true; + } + break; + + case KEY_RIGHT: + case KEY_CTRL('F'): + // Move cursor forward one character + if (pos == len) { + beep(); + } else { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + redraw = true; + } + break; + + case KEY_HOME: + case KEY_CTRL('A'): + // Move cursor to start of string + pos = 0; + cpos = 0; + redraw = true; + break; + + case KEY_END: + case KEY_CTRL('E'): + // Move cursor to end of string + pos = len; + cpos = MIN(pos, fieldsize - 1); + redraw = true; + break; + + case KEY_CLEFT: + // Move cursor to start of current or previous word + while ((pos > 0) && (! isalnum(buf[pos - 1]))) { + pos--; + if (cpos > 0) { + cpos--; + } + } + while ((pos > 0) && isalnum(buf[pos - 1])) { + pos--; + if (cpos > 0) { + cpos--; + } + } + redraw = true; + break; + + case KEY_CRIGHT: + // Move cursor to end of current or next word + while ((pos < len) && (! isalnum(buf[pos]))) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + while ((pos < len) && isalnum(buf[pos])) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + redraw = true; + break; + + // Deletion keys + + case KEY_BS: + case KEY_BACKSPACE: + case KEY_DEL: + // Delete previous character + if (pos == 0) { + beep(); + } else { + memmove(buf + pos - 1, buf + pos, len - pos + 1); + len--; + pos--; + if (cpos > 0) { + cpos--; + } + redraw = true; + } + break; + + case KEY_DC: + case KEY_CTRL('D'): + // Delete character under cursor + if (pos == len) { + beep(); + } else { + memmove(buf + pos, buf + pos + 1, len - pos); + len--; + // pos and cpos stay the same + redraw = true; + } + break; + + case KEY_CLEAR: + // Delete the entire line + strcpy(buf, ""); + len = 0; + pos = 0; + cpos = 0; + redraw = true; + break; + + case KEY_CTRL('U'): + // Delete backwards to the start of the line + if (pos == 0) { + beep(); + } else { + memmove(buf, buf + pos, len - pos + 1); + len -= pos; + pos = 0; + cpos = 0; + redraw = true; + } + break; + + case KEY_CTRL('K'): + // Delete to the end of the line + if (pos == len) { + beep(); + } else { + buf[pos] = '\0'; + len = pos; + // pos and cpos stay the same + redraw = true; + } + break; + + case KEY_CTRL('W'): + // Delete the previous word + if (pos == 0) { + beep(); + } else { + /* + Note the use of isspace() instead of isalnum(): + this makes ^W follow GNU Bash standards, which + behaves differently from Meta-DEL. + */ + + i = pos; + while ((i > 0) && isspace(buf[i - 1])) { + i--; + if (cpos > 0) { + cpos--; + } + } + while ((i > 0) && (! isspace(buf[i - 1]))) { + i--; + if (cpos > 0) { + cpos--; + } + } + + memmove(buf + i, buf + pos, len - pos + 1); + len -= (pos - i); + pos = i; + redraw = true; + } + break; + + // Miscellaneous keys and events + + case KEY_CTRL('T'): + // Transpose characters + if ((pos == 0) || (len <= 1)) { + beep(); + } else if (pos == len) { + c = buf[pos - 1]; + buf[pos - 1] = buf[pos - 2]; + buf[pos - 2] = c; + redraw = true; + } else { + c = buf[pos]; + buf[pos] = buf[pos - 1]; + buf[pos - 1] = c; + + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + redraw = true; + } + break; + + case KEY_ESC: + // Handle Meta-X-style and other function key presses + wtimeout(win, META_TIMEOUT); + key2 = wgetch(win); + + if (key2 == ERR) { + // by itself: cancel entering the string + ret = ERR; + done = true; + } else if ((key2 == 'O') || (key2 == '[')) { + // Swallow any unknown VT100-style function keys + key2 = wgetch(win); + while ((key2 != ERR) && isascii(key2) && + (strchr("0123456789;", key2) != NULL) && + (strchr("~ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz", key2) == NULL)) { + key2 = wgetch(win); + } + beep(); + } else { + // Handle Meta-X-style keypress + switch (key2) { + + // Cursor movement keys + + case 'B': + case 'b': + // Move cursor to start of current or previous word + while ((pos > 0) && (! isalnum(buf[pos - 1]))) { + pos--; + if (cpos > 0) { + cpos--; + } + } + while ((pos > 0) && isalnum(buf[pos - 1])) { + pos--; + if (cpos > 0) { + cpos--; + } + } + redraw = true; + break; + + case 'F': + case 'f': + // Move cursor to end of current or next word + while ((pos < len) && (! isalnum(buf[pos]))) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + while ((pos < len) && isalnum(buf[pos])) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + redraw = true; + break; + + // Deletion keys + + case KEY_BS: + case KEY_BACKSPACE: + case KEY_DEL: + // Delete the previous word (different from ^W) + i = pos; + while ((i > 0) && (! isalnum(buf[i - 1]))) { + i--; + if (cpos > 0) { + cpos--; + } + } + while ((i > 0) && isalnum(buf[i - 1])) { + i--; + if (cpos > 0) { + cpos--; + } + } + + memmove(buf + i, buf + pos, len - pos + 1); + len -= (pos - i); + pos = i; + redraw = true; + break; + + case 'D': + case 'd': + // Delete the next word + i = pos; + while ((i < len) && (! isalnum(buf[i]))) { + i++; + } + while ((i < len) && isalnum(buf[i])) { + i++; + } + + memmove(buf + pos, buf + i, len - i + 1); + len -= (i - pos); + // pos and cpos stay the same + redraw = true; + break; + + case '\\': + case ' ': + // Delete all surrounding spaces; if key2 == ' ', + // also insert one space + i = pos; + while ((pos > 0) && isspace(buf[pos - 1])) { + pos--; + if (cpos > 0) { + cpos--; + } + } + while ((i < len) && isspace(buf[i])) { + i++; + } + + memmove(buf + pos, buf + i, len - i + 1); + len -= (i - pos); + + if (key2 == ' ') { + if ((len >= maxlen) || ((allowed != NULL) && + (strchr(allowed, key) == NULL))) { + beep(); + } else { + memmove(buf + pos + 1, buf + pos, len - pos + 1); + buf[pos] = ' '; + len++; + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + } + + redraw = true; + + break; + + // Transformation keys + + case 'U': + case 'u': + // Convert word (from cursor onwards) to upper case + while ((pos < len) && (! isalnum(buf[pos]))) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + while ((pos < len) && isalnum(buf[pos])) { + buf[pos] = toupper(buf[pos]); + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + redraw = true; + break; + + case 'L': + case 'l': + // Convert word (from cursor onwards) to lower case + while ((pos < len) && (! isalnum(buf[pos]))) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + while ((pos < len) && isalnum(buf[pos])) { + buf[pos] = tolower(buf[pos]); + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + redraw = true; + break; + + case 'C': + case 'c': + // Convert current letter to upper case, following + // letters to lower case + first = true; + while ((pos < len) && (! isalnum(buf[pos]))) { + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + while ((pos < len) && isalnum(buf[pos])) { + if (first) { + buf[pos] = toupper(buf[pos]); + first = false; + } else { + buf[pos] = tolower(buf[pos]); + } + pos++; + if (cpos < fieldsize - 1) { + cpos++; + } + } + redraw = true; + break; + + // Miscellaneous keys and events + + case KEY_RESIZE: + case KEY_EVENT: + ret = key; + done = true; + break; + + default: + beep(); + break; + } + } + + wtimeout(win, -1); + break; + + case KEY_RESIZE: + case KEY_EVENT: + ret = key; + done = true; + break; + + default: + beep(); + break; + } + } + } + + wattrset(win, oldattr); + curs_set(CURS_OFF); + + wattron(win, A_BOLD); + mvwprintw(win, y, x, "%-*.*s", fieldsize, fieldsize, buf); + wattrset(win, oldattr); + wrefresh(win); + + return OK; +} diff --git a/src/intf.h b/src/intf.h index db34d4a..1a185d1 100644 --- a/src/intf.h +++ b/src/intf.h @@ -51,9 +51,33 @@ typedef enum curs_type { // Keycodes -#define KEY_TAB (011) -#define KEY_RETURN (012) -#define KEY_ESC (033) +#define KEY_BS (0010) +#define KEY_TAB (0011) +#define KEY_RETURN (0012) +#define KEY_ESC (0033) +#define KEY_DEL (0177) + +#define KEY_CTRL(x) (x - 0100) + +// Control-arrow key combinations +#ifndef KEY_CDOWN +# define KEY_CDOWN (01007) +#endif +#ifndef KEY_CUP +# define KEY_CUP (01060) +#endif +#ifndef KEY_CLEFT +# define KEY_CLEFT (01033) +#endif +#ifndef KEY_CRIGHT +# define KEY_CRIGHT (01052) +#endif + +// Keycode for inserting the default value +#define KEY_DEFAULTVAL '=' + +// Timeout value (in ms) for Meta-X-style keyboard input +#define META_TIMEOUT (1000) /* @@ -124,6 +148,10 @@ extern int attrpr (WINDOW *win, int attr, const char *format, ...) extern int gettxchar (WINDOW *win); extern bool getanswer (WINDOW *win); +extern int gettxline (WINDOW *win, char *buf, int bufsize, bool multifield, + int maxlen, const char *emptyval, const char *defaultval, + const char *allowed, bool stripspc, int y, int x, + int fieldsize, int attr); #endif /* included_INTF_H */ diff --git a/src/system.h b/src/system.h index 939ef41..da54df2 100644 --- a/src/system.h +++ b/src/system.h @@ -47,6 +47,8 @@ #define _GNU_SOURCE 1 +#include + #include #include #include diff --git a/src/utils.h b/src/utils.h index 03b8080..b7fee37 100644 --- a/src/utils.h +++ b/src/utils.h @@ -40,6 +40,16 @@ #define GAME_FILENAME_BUFSIZE (16) +/************************************************************************ +* Utility macro definitions * +************************************************************************/ + +// Type-unsafe minimum and maximum macros + +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + + /************************************************************************ * Utility function declarations * ************************************************************************/