freebsd-skq/contrib/dialog/ui_getc.c
2021-02-26 10:16:49 +01:00

682 lines
14 KiB
C

/*
* $Id: ui_getc.c,v 1.80 2020/11/25 01:08:30 tom Exp $
*
* ui_getc.c - user interface glue for getc()
*
* Copyright 2001-2019,2020 Thomas E. Dickey
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License, version 2.1
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to
* Free Software Foundation, Inc.
* 51 Franklin St., Fifth Floor
* Boston, MA 02110, USA.
*/
#include <dialog.h>
#include <dlg_keys.h>
#include <dlg_internals.h>
#ifdef NEED_WCHAR_H
#include <wchar.h>
#endif
#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# if HAVE_SYS_TIME_H
# include <sys/time.h>
# else
# include <time.h>
# endif
#endif
#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
#ifdef __QNX__
#include <sys/select.h>
#endif
#ifndef WEXITSTATUS
# ifdef HAVE_TYPE_UNIONWAIT
# define WEXITSTATUS(status) (status.w_retcode)
# else
# define WEXITSTATUS(status) (((status) & 0xff00) >> 8)
# endif
#endif
#ifndef WTERMSIG
# ifdef HAVE_TYPE_UNIONWAIT
# define WTERMSIG(status) (status.w_termsig)
# else
# define WTERMSIG(status) ((status) & 0x7f)
# endif
#endif
void
dlg_add_callback(DIALOG_CALLBACK * p)
{
p->next = dialog_state.getc_callbacks;
dialog_state.getc_callbacks = p;
dlg_set_timeout(p->win, TRUE);
}
/*
* Like dlg_add_callback(), but providing for cleanup of caller's associated
* state.
*/
void
dlg_add_callback_ref(DIALOG_CALLBACK ** p, DIALOG_FREEBACK freeback)
{
(*p)->caller = p;
(*p)->freeback = freeback;
dlg_add_callback(*p);
}
void
dlg_remove_callback(DIALOG_CALLBACK * p)
{
DIALOG_CALLBACK *q;
if (p->input != 0) {
FILE *input = p->input;
fclose(input);
if (p->input == dialog_state.pipe_input)
dialog_state.pipe_input = 0;
/* more than one callback can have the same input */
for (q = dialog_state.getc_callbacks; q != 0; q = q->next) {
if (q->input == input) {
q->input = 0;
}
}
}
if (!(p->keep_win))
dlg_del_window(p->win);
if ((q = dialog_state.getc_callbacks) == p) {
dialog_state.getc_callbacks = p->next;
} else {
while (q != 0) {
if (q->next == p) {
q->next = p->next;
break;
}
q = q->next;
}
}
/* handle dlg_add_callback_ref cleanup */
if (p->freeback != 0)
p->freeback(p);
if (p->caller != 0)
*(p->caller) = 0;
free(p);
}
/*
* A select() might find more than one input ready for service. Handle them
* all.
*/
static bool
handle_inputs(WINDOW *win)
{
bool result = FALSE;
DIALOG_CALLBACK *p;
DIALOG_CALLBACK *q;
int cur_y, cur_x;
int state = ERR;
getyx(win, cur_y, cur_x);
for (p = dialog_state.getc_callbacks, q = 0; p != 0; p = q) {
q = p->next;
if ((p->handle_input != 0) && p->input_ready) {
p->input_ready = FALSE;
if (state == ERR) {
state = curs_set(0);
}
if (p->handle_input(p)) {
result = TRUE;
}
}
}
if (result && _dlg_find_window(win)) {
(void) wmove(win, cur_y, cur_x); /* Restore cursor position */
wrefresh(win);
} else {
result = FALSE;
}
if (state != ERR)
curs_set(state);
return result;
}
static bool
may_handle_inputs(void)
{
bool result = FALSE;
DIALOG_CALLBACK *p;
for (p = dialog_state.getc_callbacks; p != 0; p = p->next) {
if (p->input != 0) {
result = TRUE;
break;
}
}
return result;
}
/*
* Check any any inputs registered via callbacks, to see if there is any input
* available. If there is, return a file-descriptor which should be read.
* Otherwise, return -1.
*/
static int
check_inputs(void)
{
DIALOG_CALLBACK *p;
fd_set read_fds;
struct timeval test;
int result = -1;
if ((p = dialog_state.getc_callbacks) != 0) {
int last_fd = -1;
int found;
int fd;
FD_ZERO(&read_fds);
while (p != 0) {
p->input_ready = FALSE;
if (p->input != 0 && (fd = fileno(p->input)) >= 0) {
FD_SET(fd, &read_fds);
if (last_fd < fd)
last_fd = fd;
}
p = p->next;
}
test.tv_sec = 0;
test.tv_usec = WTIMEOUT_VAL * 1000;
found = select(last_fd + 1, &read_fds,
(fd_set *) 0,
(fd_set *) 0,
&test);
if (found > 0) {
for (p = dialog_state.getc_callbacks; p != 0; p = p->next) {
if (p->input != 0
&& (fd = fileno(p->input)) >= 0
&& FD_ISSET(fd, &read_fds)) {
p->input_ready = TRUE;
result = fd;
}
}
}
}
return result;
}
int
dlg_getc_callbacks(int ch, int fkey, int *result)
{
int code = FALSE;
DIALOG_CALLBACK *p, *q;
if ((p = dialog_state.getc_callbacks) != 0) {
if (check_inputs() >= 0) {
do {
q = p->next;
if (p->input_ready) {
if (!(p->handle_getc(p, ch, fkey, result))) {
dlg_remove_callback(p);
}
}
} while ((p = q) != 0);
}
code = (dialog_state.getc_callbacks != 0);
}
return code;
}
static void
dlg_raise_window(WINDOW *win)
{
if (_dlg_find_window(win)) {
touchwin(win);
wmove(win, getcury(win), getcurx(win));
wnoutrefresh(win);
doupdate();
}
}
/*
* This is a work-around for the case where we actually need the wide-character
* code versus a byte stream.
*/
static int last_getc = ERR;
#ifdef USE_WIDE_CURSES
static char last_getc_bytes[80];
static int have_last_getc;
static int used_last_getc;
#endif
int
dlg_last_getc(void)
{
#ifdef USE_WIDE_CURSES
if (used_last_getc != 1)
return ERR; /* not really an error... */
#endif
return last_getc;
}
void
dlg_flush_getc(void)
{
last_getc = ERR;
#ifdef USE_WIDE_CURSES
have_last_getc = 0;
used_last_getc = 0;
#endif
}
/*
* Report the last key entered by the user. The 'mode' parameter controls
* the way it is separated from other results:
* -2 (no separator)
* -1 (separator after the key name)
* 0 (separator is optionally before the key name)
* 1 (same as -1)
*/
void
dlg_add_last_key(int mode)
{
if (dialog_vars.last_key) {
if (mode >= 0) {
if (mode > 0) {
dlg_add_last_key(-1);
} else {
if (dlg_need_separator())
dlg_add_separator();
dlg_add_last_key(-2);
}
} else {
char temp[80];
sprintf(temp, "%d", last_getc);
DLG_TRACE(("# dlg_add_last_key(%s)\n", temp));
dlg_add_string(temp);
if (mode == -1)
dlg_add_separator();
}
}
}
/*
* Check if the stream has been unexpectedly closed, returning false in that
* case.
*/
static bool
valid_file(FILE *fp)
{
bool code = FALSE;
int fd = fileno(fp);
if (fd >= 0) {
if (fcntl(fd, F_GETFL, 0) >= 0) {
code = TRUE;
}
}
return code;
}
static int
really_getch(WINDOW *win, int *fkey)
{
int ch;
#ifdef USE_WIDE_CURSES
mbstate_t state;
wint_t my_wint;
/*
* We get a wide character, translate it to multibyte form to avoid
* having to change the rest of the code to use wide-characters.
*/
if (used_last_getc >= have_last_getc) {
int code;
wchar_t my_wchar;
used_last_getc = 0;
have_last_getc = 0;
ch = ERR;
*fkey = 0;
code = wget_wch(win, &my_wint);
my_wchar = (wchar_t) my_wint;
switch (code) {
case KEY_CODE_YES:
ch = *fkey = my_wchar;
last_getc = my_wchar;
break;
case OK:
memset(&state, 0, sizeof(state));
have_last_getc = (int) wcrtomb(last_getc_bytes, my_wchar, &state);
if (have_last_getc < 0) {
have_last_getc = used_last_getc = 0;
last_getc_bytes[0] = (char) my_wchar;
}
ch = (int) CharOf(last_getc_bytes[used_last_getc++]);
last_getc = my_wchar;
break;
case ERR:
ch = ERR;
last_getc = ERR;
break;
default:
break;
}
} else {
ch = (int) CharOf(last_getc_bytes[used_last_getc++]);
}
#else
ch = wgetch(win);
last_getc = ch;
*fkey = (ch > KEY_MIN && ch < KEY_MAX);
#endif
return ch;
}
static DIALOG_CALLBACK *
next_callback(DIALOG_CALLBACK * p)
{
if ((p = dialog_state.getc_redirect) != 0) {
p = p->next;
} else {
p = dialog_state.getc_callbacks;
}
return p;
}
static DIALOG_CALLBACK *
prev_callback(DIALOG_CALLBACK * p)
{
DIALOG_CALLBACK *q;
if ((p = dialog_state.getc_redirect) != 0) {
if (p == dialog_state.getc_callbacks) {
for (p = dialog_state.getc_callbacks; p->next != 0; p = p->next) ;
} else {
for (q = dialog_state.getc_callbacks; q->next != p; q = q->next) ;
p = q;
}
} else {
p = dialog_state.getc_callbacks;
}
return p;
}
#define isBeforeChr(chr) ((chr) == before_chr && !before_fkey)
#define isBeforeFkey(chr) ((chr) == before_chr && before_fkey)
/*
* Read a character from the given window. Handle repainting here (to simplify
* things in the calling application). Also, if input-callback(s) are set up,
* poll the corresponding files and handle the updates, e.g., for displaying a
* tailbox.
*/
int
dlg_getc(WINDOW *win, int *fkey)
{
WINDOW *save_win = win;
int ch = ERR;
int before_chr;
int before_fkey;
int result;
bool done = FALSE;
bool literal = FALSE;
DIALOG_CALLBACK *p = 0;
int interval = dlg_set_timeout(win, may_handle_inputs());
time_t expired = time((time_t *) 0) + dialog_vars.timeout_secs;
time_t current;
while (!done) {
bool handle_others = FALSE;
if (_dlg_find_window(win) == NULL)
break;
/*
* If there was no pending file-input, check the keyboard.
*/
ch = really_getch(win, fkey);
if (literal) {
done = TRUE;
continue;
}
before_chr = ch;
before_fkey = *fkey;
ch = dlg_lookup_key(win, ch, fkey);
dlg_trace_chr(ch, *fkey);
current = time((time_t *) 0);
/*
* If we acquired a fkey value, then it is one of dialog's builtin
* codes such as DLGK_HELPFILE.
*/
if (!*fkey || *fkey != before_fkey) {
switch (ch) {
case CHR_LITERAL:
literal = TRUE;
keypad(win, FALSE);
continue;
case CHR_REPAINT:
if (_dlg_find_window(win)) {
(void) touchwin(win);
(void) wrefresh(curscr);
}
break;
case ERR: /* wtimeout() in effect; check for file I/O */
if (interval > 0
&& current >= expired) {
int status;
DLG_TRACE(("# dlg_getc: timeout expired\n"));
if (dlg_getenv_num("DIALOG_TIMEOUT", &status)) {
dlg_exiterr("timeout");
}
ch = ESC;
done = TRUE;
} else if (!valid_file(stdin)
|| !valid_file(dialog_state.screen_output)) {
DLG_TRACE(("# dlg_getc: input or output is invalid\n"));
ch = ESC;
done = TRUE;
} else if (check_inputs()) {
if (_dlg_find_window(win) && handle_inputs(win))
dlg_raise_window(win);
else
done = TRUE;
} else {
done = (interval <= 0);
}
break;
case DLGK_HELPFILE:
if (dialog_vars.help_file && _dlg_find_window(win)) {
int yold, xold;
getyx(win, yold, xold);
dialog_helpfile("HELP", dialog_vars.help_file, 0, 0);
dlg_raise_window(win);
wmove(win, yold, xold);
}
continue;
case DLGK_FIELD_PREV:
/* FALLTHRU */
case KEY_BTAB:
/* FALLTHRU */
case DLGK_FIELD_NEXT:
/* FALLTHRU */
case TAB:
/* Handle tab/backtab as a special case for traversing between
* the nominal "current" window, and other windows having
* callbacks. If the nominal (control) window closes, we'll
* close the windows with callbacks.
*/
if (dialog_state.getc_callbacks != 0 &&
(isBeforeChr(TAB) ||
isBeforeFkey(KEY_BTAB))) {
p = (isBeforeChr(TAB)
? next_callback(p)
: prev_callback(p));
if ((dialog_state.getc_redirect = p) != 0) {
win = p->win;
} else {
win = save_win;
}
dlg_raise_window(win);
break;
}
/* FALLTHRU */
default:
#ifdef NO_LEAKS
if (isBeforeChr(DLG_CTRL('P'))) {
/* for testing, ^P closes the connection */
close(0);
close(1);
close(2);
break;
}
#endif
handle_others = TRUE;
break;
#ifdef HAVE_DLG_TRACE
case CHR_TRACE:
dlg_trace_win(win);
break;
#endif
}
} else {
handle_others = TRUE;
}
if (handle_others) {
if ((p = dialog_state.getc_redirect) != 0) {
if (!(p->handle_getc(p, ch, *fkey, &result))) {
done = (p->win == save_win) && (!p->keep_win);
dlg_remove_callback(p);
dialog_state.getc_redirect = 0;
win = save_win;
}
} else {
done = TRUE;
}
}
}
if (literal && _dlg_find_window(win))
keypad(win, TRUE);
return ch;
}
static void
finish_bg(int sig GCC_UNUSED)
{
end_dialog();
dlg_exit(DLG_EXIT_ERROR);
}
/*
* If we have callbacks active, purge the list of all that are not marked
* to keep in the background. If any remain, run those in a background
* process.
*/
void
dlg_killall_bg(int *retval)
{
DIALOG_CALLBACK *cb;
#ifdef HAVE_TYPE_UNIONWAIT
union wait wstatus;
#else
int wstatus;
#endif
if ((cb = dialog_state.getc_callbacks) != 0) {
while (cb != 0) {
if (cb->keep_bg) {
cb = cb->next;
} else {
dlg_remove_callback(cb);
cb = dialog_state.getc_callbacks;
}
}
if (dialog_state.getc_callbacks != 0) {
int pid;
refresh();
fflush(stdout);
fflush(stderr);
reset_shell_mode();
if ((pid = fork()) != 0) {
_exit(pid > 0 ? DLG_EXIT_OK : DLG_EXIT_ERROR);
} else { /* child, pid==0 */
if ((pid = fork()) != 0) {
/*
* Echo the process-id of the grandchild so a shell script
* can read that, and kill that process. We'll wait around
* until then. Our parent has already left, leaving us
* temporarily orphaned.
*/
if (pid > 0) { /* parent */
fprintf(stderr, "%d\n", pid);
fflush(stderr);
}
/* wait for child */
#ifdef HAVE_WAITPID
while (-1 == waitpid(pid, &wstatus, 0)) {
#ifdef EINTR
if (errno == EINTR)
continue;
#endif /* EINTR */
#ifdef ERESTARTSYS
if (errno == ERESTARTSYS)
continue;
#endif /* ERESTARTSYS */
break;
}
#else
while (wait(&wstatus) != pid) /* do nothing */
;
#endif
_exit(WEXITSTATUS(wstatus));
} else { /* child, pid==0 */
if (!dialog_vars.cant_kill)
(void) signal(SIGHUP, finish_bg);
(void) signal(SIGINT, finish_bg);
(void) signal(SIGQUIT, finish_bg);
(void) signal(SIGSEGV, finish_bg);
while (dialog_state.getc_callbacks != 0) {
int fkey = 0;
dlg_getc_callbacks(ERR, fkey, retval);
napms(1000);
}
}
}
}
}
}