xref: /freebsd/contrib/dialog/ui_getc.c (revision 19718649)
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 
67 void
dlg_add_callback(DIALOG_CALLBACK * p)68 dlg_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  */
79 void
dlg_add_callback_ref(DIALOG_CALLBACK ** p,DIALOG_FREEBACK freeback)80 dlg_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 
87 void
dlg_remove_callback(DIALOG_CALLBACK * p)88 dlg_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  */
132 static bool
handle_inputs(WINDOW * win)133 handle_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 
165 static bool
may_handle_inputs(void)166 may_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  */
187 static int
check_inputs(void)188 check_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 
235 int
dlg_getc_callbacks(int ch,int fkey,int * result)236 dlg_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 
257 static void
dlg_raise_window(WINDOW * win)258 dlg_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  */
272 static int last_getc = ERR;
273 
274 #ifdef USE_WIDE_CURSES
275 static char last_getc_bytes[80];
276 static int have_last_getc;
277 static int used_last_getc;
278 #endif
279 
280 int
dlg_last_getc(void)281 dlg_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 
290 void
dlg_flush_getc(void)291 dlg_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  */
308 void
dlg_add_last_key(int mode)309 dlg_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  */
335 static bool
valid_file(FILE * fp)336 valid_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 
349 static int
really_getch(WINDOW * win,int * fkey)350 really_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 
404 static DIALOG_CALLBACK *
next_callback(DIALOG_CALLBACK * p)405 next_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 
415 static DIALOG_CALLBACK *
prev_callback(DIALOG_CALLBACK * p)416 prev_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  */
442 int
dlg_getc(WINDOW * win,int * fkey)443 dlg_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 
596 static void
finish_bg(int sig GCC_UNUSED)597 finish_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  */
608 void
dlg_killall_bg(int * retval)609 dlg_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