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