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