1/*
2 *  $Id: ui_getc.c,v 1.80 2020/11/25 01:08:30 tom Exp $
3 *
4 *  ui_getc.c - user interface glue for getc()
5 *
6 *  Copyright 2001-2019,2020	Thomas E. Dickey
7 *
8 *  This program is free software; you can redistribute it and/or modify
9 *  it under the terms of the GNU Lesser General Public License, version 2.1
10 *  as published by the Free Software Foundation.
11 *
12 *  This program is distributed in the hope that it will be useful, but
13 *  WITHOUT ANY WARRANTY; without even the implied warranty of
14 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 *  Lesser General Public License for more details.
16 *
17 *  You should have received a copy of the GNU Lesser General Public
18 *  License along with this program; if not, write to
19 *	Free Software Foundation, Inc.
20 *	51 Franklin St., Fifth Floor
21 *	Boston, MA 02110, USA.
22 */
23
24#include <dialog.h>
25#include <dlg_keys.h>
26#include <dlg_internals.h>
27
28#ifdef NEED_WCHAR_H
29#include <wchar.h>
30#endif
31
32#if TIME_WITH_SYS_TIME
33# include <sys/time.h>
34# include <time.h>
35#else
36# if HAVE_SYS_TIME_H
37#  include <sys/time.h>
38# else
39#  include <time.h>
40# endif
41#endif
42
43#ifdef HAVE_SYS_WAIT_H
44#include <sys/wait.h>
45#endif
46
47#ifdef __QNX__
48#include <sys/select.h>
49#endif
50
51#ifndef WEXITSTATUS
52# ifdef HAVE_TYPE_UNIONWAIT
53#  define	WEXITSTATUS(status)	(status.w_retcode)
54# else
55#  define	WEXITSTATUS(status)	(((status) & 0xff00) >> 8)
56# endif
57#endif
58
59#ifndef WTERMSIG
60# ifdef HAVE_TYPE_UNIONWAIT
61#  define	WTERMSIG(status)	(status.w_termsig)
62# else
63#  define	WTERMSIG(status)	((status) & 0x7f)
64# endif
65#endif
66
67void
68dlg_add_callback(DIALOG_CALLBACK * p)
69{
70    p->next = dialog_state.getc_callbacks;
71    dialog_state.getc_callbacks = p;
72    dlg_set_timeout(p->win, TRUE);
73}
74
75/*
76 * Like dlg_add_callback(), but providing for cleanup of caller's associated
77 * state.
78 */
79void
80dlg_add_callback_ref(DIALOG_CALLBACK ** p, DIALOG_FREEBACK freeback)
81{
82    (*p)->caller = p;
83    (*p)->freeback = freeback;
84    dlg_add_callback(*p);
85}
86
87void
88dlg_remove_callback(DIALOG_CALLBACK * p)
89{
90    DIALOG_CALLBACK *q;
91
92    if (p->input != 0) {
93	FILE *input = p->input;
94	fclose(input);
95	if (p->input == dialog_state.pipe_input)
96	    dialog_state.pipe_input = 0;
97	/* more than one callback can have the same input */
98	for (q = dialog_state.getc_callbacks; q != 0; q = q->next) {
99	    if (q->input == input) {
100		q->input = 0;
101	    }
102	}
103    }
104
105    if (!(p->keep_win))
106	dlg_del_window(p->win);
107    if ((q = dialog_state.getc_callbacks) == p) {
108	dialog_state.getc_callbacks = p->next;
109    } else {
110	while (q != 0) {
111	    if (q->next == p) {
112		q->next = p->next;
113		break;
114	    }
115	    q = q->next;
116	}
117    }
118
119    /* handle dlg_add_callback_ref cleanup */
120    if (p->freeback != 0)
121	p->freeback(p);
122    if (p->caller != 0)
123	*(p->caller) = 0;
124
125    free(p);
126}
127
128/*
129 * A select() might find more than one input ready for service.  Handle them
130 * all.
131 */
132static bool
133handle_inputs(WINDOW *win)
134{
135    bool result = FALSE;
136    DIALOG_CALLBACK *p;
137    DIALOG_CALLBACK *q;
138    int cur_y, cur_x;
139    int state = ERR;
140
141    getyx(win, cur_y, cur_x);
142    for (p = dialog_state.getc_callbacks, q = 0; p != 0; p = q) {
143	q = p->next;
144	if ((p->handle_input != 0) && p->input_ready) {
145	    p->input_ready = FALSE;
146	    if (state == ERR) {
147		state = curs_set(0);
148	    }
149	    if (p->handle_input(p)) {
150		result = TRUE;
151	    }
152	}
153    }
154    if (result && _dlg_find_window(win)) {
155	(void) wmove(win, cur_y, cur_x);	/* Restore cursor position */
156	wrefresh(win);
157    } else {
158	result = FALSE;
159    }
160    if (state != ERR)
161	curs_set(state);
162    return result;
163}
164
165static bool
166may_handle_inputs(void)
167{
168    bool result = FALSE;
169
170    DIALOG_CALLBACK *p;
171
172    for (p = dialog_state.getc_callbacks; p != 0; p = p->next) {
173	if (p->input != 0) {
174	    result = TRUE;
175	    break;
176	}
177    }
178
179    return result;
180}
181
182/*
183 * Check any any inputs registered via callbacks, to see if there is any input
184 * available.  If there is, return a file-descriptor which should be read.
185 * Otherwise, return -1.
186 */
187static int
188check_inputs(void)
189{
190    DIALOG_CALLBACK *p;
191    fd_set read_fds;
192    struct timeval test;
193    int result = -1;
194
195    if ((p = dialog_state.getc_callbacks) != 0) {
196	int last_fd = -1;
197	int found;
198	int fd;
199
200	FD_ZERO(&read_fds);
201
202	while (p != 0) {
203
204	    p->input_ready = FALSE;
205	    if (p->input != 0 && (fd = fileno(p->input)) >= 0) {
206		FD_SET(fd, &read_fds);
207		if (last_fd < fd)
208		    last_fd = fd;
209	    }
210	    p = p->next;
211	}
212
213	test.tv_sec = 0;
214	test.tv_usec = WTIMEOUT_VAL * 1000;
215	found = select(last_fd + 1, &read_fds,
216		       (fd_set *) 0,
217		       (fd_set *) 0,
218		       &test);
219
220	if (found > 0) {
221	    for (p = dialog_state.getc_callbacks; p != 0; p = p->next) {
222		if (p->input != 0
223		    && (fd = fileno(p->input)) >= 0
224		    && FD_ISSET(fd, &read_fds)) {
225		    p->input_ready = TRUE;
226		    result = fd;
227		}
228	    }
229	}
230    }
231
232    return result;
233}
234
235int
236dlg_getc_callbacks(int ch, int fkey, int *result)
237{
238    int code = FALSE;
239    DIALOG_CALLBACK *p, *q;
240
241    if ((p = dialog_state.getc_callbacks) != 0) {
242	if (check_inputs() >= 0) {
243	    do {
244		q = p->next;
245		if (p->input_ready) {
246		    if (!(p->handle_getc(p, ch, fkey, result))) {
247			dlg_remove_callback(p);
248		    }
249		}
250	    } while ((p = q) != 0);
251	}
252	code = (dialog_state.getc_callbacks != 0);
253    }
254    return code;
255}
256
257static void
258dlg_raise_window(WINDOW *win)
259{
260    if (_dlg_find_window(win)) {
261	touchwin(win);
262	wmove(win, getcury(win), getcurx(win));
263	wnoutrefresh(win);
264	doupdate();
265    }
266}
267
268/*
269 * This is a work-around for the case where we actually need the wide-character
270 * code versus a byte stream.
271 */
272static int last_getc = ERR;
273
274#ifdef USE_WIDE_CURSES
275static char last_getc_bytes[80];
276static int have_last_getc;
277static int used_last_getc;
278#endif
279
280int
281dlg_last_getc(void)
282{
283#ifdef USE_WIDE_CURSES
284    if (used_last_getc != 1)
285	return ERR;		/* not really an error... */
286#endif
287    return last_getc;
288}
289
290void
291dlg_flush_getc(void)
292{
293    last_getc = ERR;
294#ifdef USE_WIDE_CURSES
295    have_last_getc = 0;
296    used_last_getc = 0;
297#endif
298}
299
300/*
301 * Report the last key entered by the user.  The 'mode' parameter controls
302 * the way it is separated from other results:
303 * -2 (no separator)
304 * -1 (separator after the key name)
305 * 0 (separator is optionally before the key name)
306 * 1 (same as -1)
307 */
308void
309dlg_add_last_key(int mode)
310{
311    if (dialog_vars.last_key) {
312	if (mode >= 0) {
313	    if (mode > 0) {
314		dlg_add_last_key(-1);
315	    } else {
316		if (dlg_need_separator())
317		    dlg_add_separator();
318		dlg_add_last_key(-2);
319	    }
320	} else {
321	    char temp[80];
322	    sprintf(temp, "%d", last_getc);
323	    DLG_TRACE(("# dlg_add_last_key(%s)\n", temp));
324	    dlg_add_string(temp);
325	    if (mode == -1)
326		dlg_add_separator();
327	}
328    }
329}
330
331/*
332 * Check if the stream has been unexpectedly closed, returning false in that
333 * case.
334 */
335static bool
336valid_file(FILE *fp)
337{
338    bool code = FALSE;
339    int fd = fileno(fp);
340
341    if (fd >= 0) {
342	if (fcntl(fd, F_GETFL, 0) >= 0) {
343	    code = TRUE;
344	}
345    }
346    return code;
347}
348
349static int
350really_getch(WINDOW *win, int *fkey)
351{
352    int ch;
353#ifdef USE_WIDE_CURSES
354    mbstate_t state;
355    wint_t my_wint;
356
357    /*
358     * We get a wide character, translate it to multibyte form to avoid
359     * having to change the rest of the code to use wide-characters.
360     */
361    if (used_last_getc >= have_last_getc) {
362	int code;
363	wchar_t my_wchar;
364
365	used_last_getc = 0;
366	have_last_getc = 0;
367	ch = ERR;
368	*fkey = 0;
369	code = wget_wch(win, &my_wint);
370	my_wchar = (wchar_t) my_wint;
371	switch (code) {
372	case KEY_CODE_YES:
373	    ch = *fkey = my_wchar;
374	    last_getc = my_wchar;
375	    break;
376	case OK:
377	    memset(&state, 0, sizeof(state));
378	    have_last_getc = (int) wcrtomb(last_getc_bytes, my_wchar, &state);
379	    if (have_last_getc < 0) {
380		have_last_getc = used_last_getc = 0;
381		last_getc_bytes[0] = (char) my_wchar;
382	    }
383	    ch = (int) CharOf(last_getc_bytes[used_last_getc++]);
384	    last_getc = my_wchar;
385	    break;
386	case ERR:
387	    ch = ERR;
388	    last_getc = ERR;
389	    break;
390	default:
391	    break;
392	}
393    } else {
394	ch = (int) CharOf(last_getc_bytes[used_last_getc++]);
395    }
396#else
397    ch = wgetch(win);
398    last_getc = ch;
399    *fkey = (ch > KEY_MIN && ch < KEY_MAX);
400#endif
401    return ch;
402}
403
404static DIALOG_CALLBACK *
405next_callback(DIALOG_CALLBACK * p)
406{
407    if ((p = dialog_state.getc_redirect) != 0) {
408	p = p->next;
409    } else {
410	p = dialog_state.getc_callbacks;
411    }
412    return p;
413}
414
415static DIALOG_CALLBACK *
416prev_callback(DIALOG_CALLBACK * p)
417{
418    DIALOG_CALLBACK *q;
419
420    if ((p = dialog_state.getc_redirect) != 0) {
421	if (p == dialog_state.getc_callbacks) {
422	    for (p = dialog_state.getc_callbacks; p->next != 0; p = p->next) ;
423	} else {
424	    for (q = dialog_state.getc_callbacks; q->next != p; q = q->next) ;
425	    p = q;
426	}
427    } else {
428	p = dialog_state.getc_callbacks;
429    }
430    return p;
431}
432
433#define isBeforeChr(chr) ((chr) == before_chr && !before_fkey)
434#define isBeforeFkey(chr) ((chr) == before_chr && before_fkey)
435
436/*
437 * Read a character from the given window.  Handle repainting here (to simplify
438 * things in the calling application).  Also, if input-callback(s) are set up,
439 * poll the corresponding files and handle the updates, e.g., for displaying a
440 * tailbox.
441 */
442int
443dlg_getc(WINDOW *win, int *fkey)
444{
445    WINDOW *save_win = win;
446    int ch = ERR;
447    int before_chr;
448    int before_fkey;
449    int result;
450    bool done = FALSE;
451    bool literal = FALSE;
452    DIALOG_CALLBACK *p = 0;
453    int interval = dlg_set_timeout(win, may_handle_inputs());
454    time_t expired = time((time_t *) 0) + dialog_vars.timeout_secs;
455    time_t current;
456
457    while (!done) {
458	bool handle_others = FALSE;
459
460	if (_dlg_find_window(win) == NULL)
461	    break;
462
463	/*
464	 * If there was no pending file-input, check the keyboard.
465	 */
466	ch = really_getch(win, fkey);
467	if (literal) {
468	    done = TRUE;
469	    continue;
470	}
471
472	before_chr = ch;
473	before_fkey = *fkey;
474
475	ch = dlg_lookup_key(win, ch, fkey);
476	dlg_trace_chr(ch, *fkey);
477
478	current = time((time_t *) 0);
479
480	/*
481	 * If we acquired a fkey value, then it is one of dialog's builtin
482	 * codes such as DLGK_HELPFILE.
483	 */
484	if (!*fkey || *fkey != before_fkey) {
485	    switch (ch) {
486	    case CHR_LITERAL:
487		literal = TRUE;
488		keypad(win, FALSE);
489		continue;
490	    case CHR_REPAINT:
491		if (_dlg_find_window(win)) {
492		    (void) touchwin(win);
493		    (void) wrefresh(curscr);
494		}
495		break;
496	    case ERR:		/* wtimeout() in effect; check for file I/O */
497		if (interval > 0
498		    && current >= expired) {
499		    int status;
500		    DLG_TRACE(("# dlg_getc: timeout expired\n"));
501		    if (dlg_getenv_num("DIALOG_TIMEOUT", &status)) {
502			dlg_exiterr("timeout");
503		    }
504		    ch = ESC;
505		    done = TRUE;
506		} else if (!valid_file(stdin)
507			   || !valid_file(dialog_state.screen_output)) {
508		    DLG_TRACE(("# dlg_getc: input or output is invalid\n"));
509		    ch = ESC;
510		    done = TRUE;
511		} else if (check_inputs()) {
512		    if (_dlg_find_window(win) && handle_inputs(win))
513			dlg_raise_window(win);
514		    else
515			done = TRUE;
516		} else {
517		    done = (interval <= 0);
518		}
519		break;
520	    case DLGK_HELPFILE:
521		if (dialog_vars.help_file && _dlg_find_window(win)) {
522		    int yold, xold;
523		    getyx(win, yold, xold);
524		    dialog_helpfile("HELP", dialog_vars.help_file, 0, 0);
525		    dlg_raise_window(win);
526		    wmove(win, yold, xold);
527		}
528		continue;
529	    case DLGK_FIELD_PREV:
530		/* FALLTHRU */
531	    case KEY_BTAB:
532		/* FALLTHRU */
533	    case DLGK_FIELD_NEXT:
534		/* FALLTHRU */
535	    case TAB:
536		/* Handle tab/backtab as a special case for traversing between
537		 * the nominal "current" window, and other windows having
538		 * callbacks.  If the nominal (control) window closes, we'll
539		 * close the windows with callbacks.
540		 */
541		if (dialog_state.getc_callbacks != 0 &&
542		    (isBeforeChr(TAB) ||
543		     isBeforeFkey(KEY_BTAB))) {
544		    p = (isBeforeChr(TAB)
545			 ? next_callback(p)
546			 : prev_callback(p));
547		    if ((dialog_state.getc_redirect = p) != 0) {
548			win = p->win;
549		    } else {
550			win = save_win;
551		    }
552		    dlg_raise_window(win);
553		    break;
554		}
555		/* FALLTHRU */
556	    default:
557#ifdef NO_LEAKS
558		if (isBeforeChr(DLG_CTRL('P'))) {
559		    /* for testing, ^P closes the connection */
560		    close(0);
561		    close(1);
562		    close(2);
563		    break;
564		}
565#endif
566		handle_others = TRUE;
567		break;
568#ifdef HAVE_DLG_TRACE
569	    case CHR_TRACE:
570		dlg_trace_win(win);
571		break;
572#endif
573	    }
574	} else {
575	    handle_others = TRUE;
576	}
577
578	if (handle_others) {
579	    if ((p = dialog_state.getc_redirect) != 0) {
580		if (!(p->handle_getc(p, ch, *fkey, &result))) {
581		    done = (p->win == save_win) && (!p->keep_win);
582		    dlg_remove_callback(p);
583		    dialog_state.getc_redirect = 0;
584		    win = save_win;
585		}
586	    } else {
587		done = TRUE;
588	    }
589	}
590    }
591    if (literal && _dlg_find_window(win))
592	keypad(win, TRUE);
593    return ch;
594}
595
596static void
597finish_bg(int sig GCC_UNUSED)
598{
599    end_dialog();
600    dlg_exit(DLG_EXIT_ERROR);
601}
602
603/*
604 * If we have callbacks active, purge the list of all that are not marked
605 * to keep in the background.  If any remain, run those in a background
606 * process.
607 */
608void
609dlg_killall_bg(int *retval)
610{
611    DIALOG_CALLBACK *cb;
612#ifdef HAVE_TYPE_UNIONWAIT
613    union wait wstatus;
614#else
615    int wstatus;
616#endif
617
618    if ((cb = dialog_state.getc_callbacks) != 0) {
619	while (cb != 0) {
620	    if (cb->keep_bg) {
621		cb = cb->next;
622	    } else {
623		dlg_remove_callback(cb);
624		cb = dialog_state.getc_callbacks;
625	    }
626	}
627	if (dialog_state.getc_callbacks != 0) {
628	    int pid;
629
630	    refresh();
631	    fflush(stdout);
632	    fflush(stderr);
633	    reset_shell_mode();
634	    if ((pid = fork()) != 0) {
635		_exit(pid > 0 ? DLG_EXIT_OK : DLG_EXIT_ERROR);
636	    } else {		/* child, pid==0 */
637		if ((pid = fork()) != 0) {
638		    /*
639		     * Echo the process-id of the grandchild so a shell script
640		     * can read that, and kill that process.  We'll wait around
641		     * until then.  Our parent has already left, leaving us
642		     * temporarily orphaned.
643		     */
644		    if (pid > 0) {	/* parent */
645			fprintf(stderr, "%d\n", pid);
646			fflush(stderr);
647		    }
648		    /* wait for child */
649#ifdef HAVE_WAITPID
650		    while (-1 == waitpid(pid, &wstatus, 0)) {
651#ifdef EINTR
652			if (errno == EINTR)
653			    continue;
654#endif /* EINTR */
655#ifdef ERESTARTSYS
656			if (errno == ERESTARTSYS)
657			    continue;
658#endif /* ERESTARTSYS */
659			break;
660		    }
661#else
662		    while (wait(&wstatus) != pid)	/* do nothing */
663			;
664#endif
665		    _exit(WEXITSTATUS(wstatus));
666		} else {	/* child, pid==0 */
667		    if (!dialog_vars.cant_kill)
668			(void) signal(SIGHUP, finish_bg);
669		    (void) signal(SIGINT, finish_bg);
670		    (void) signal(SIGQUIT, finish_bg);
671		    (void) signal(SIGSEGV, finish_bg);
672		    while (dialog_state.getc_callbacks != 0) {
673			int fkey = 0;
674			dlg_getc_callbacks(ERR, fkey, retval);
675			napms(1000);
676		    }
677		}
678	    }
679	}
680    }
681}
682