xref: /freebsd/lib/libdpv/dialog_util.c (revision 069ac184)
1 /*-
2  * Copyright (c) 2013-2018 Devin Teske <dteske@FreeBSD.org>
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  */
26 
27 #include <sys/cdefs.h>
28 #include <sys/ioctl.h>
29 
30 #include <ctype.h>
31 #include <err.h>
32 #include <fcntl.h>
33 #include <limits.h>
34 #include <spawn.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <termios.h>
39 #include <unistd.h>
40 
41 #include "dialog_util.h"
42 #include "dpv.h"
43 #include "dpv_private.h"
44 
45 extern char **environ;
46 
47 #define TTY_DEFAULT_ROWS	24
48 #define TTY_DEFAULT_COLS	80
49 
50 /* [X]dialog(1) characteristics */
51 uint8_t dialog_test	= 0;
52 uint8_t use_dialog	= 0;
53 uint8_t use_libdialog	= 1;
54 uint8_t use_xdialog	= 0;
55 uint8_t use_color	= 1;
56 char dialog[PATH_MAX]	= DIALOG;
57 
58 /* [X]dialog(1) functionality */
59 char *title	= NULL;
60 char *backtitle	= NULL;
61 int dheight	= 0;
62 int dwidth	= 0;
63 static char *dargv[64] = { NULL };
64 
65 /* TTY/Screen characteristics */
66 static struct winsize *maxsize = NULL;
67 
68 /* Function prototypes */
69 static void tty_maxsize_update(void);
70 static void x11_maxsize_update(void);
71 
72 /*
73  * Update row/column fields of `maxsize' global (used by dialog_maxrows() and
74  * dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.
75  * The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current
76  * maximum height and width (respectively) for a dialog(1) widget based on the
77  * active TTY size.
78  *
79  * This function is called automatically by dialog_maxrows/cols() to reflect
80  * changes in terminal size in-between calls.
81  */
82 static void
83 tty_maxsize_update(void)
84 {
85 	int fd = STDIN_FILENO;
86 	struct termios t;
87 
88 	if (maxsize == NULL) {
89 		if ((maxsize = malloc(sizeof(struct winsize))) == NULL)
90 			errx(EXIT_FAILURE, "Out of memory?!");
91 		memset((void *)maxsize, '\0', sizeof(struct winsize));
92 	}
93 
94 	if (!isatty(fd))
95 		fd = open("/dev/tty", O_RDONLY);
96 	if ((tcgetattr(fd, &t) < 0) || (ioctl(fd, TIOCGWINSZ, maxsize) < 0)) {
97 		maxsize->ws_row = TTY_DEFAULT_ROWS;
98 		maxsize->ws_col = TTY_DEFAULT_COLS;
99 	}
100 }
101 
102 /*
103  * Update row/column fields of `maxsize' global (used by dialog_maxrows() and
104  * dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.
105  * The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current
106  * maximum height and width (respectively) for an Xdialog(1) widget based on
107  * the active video resolution of the X11 environment.
108  *
109  * This function is called automatically by dialog_maxrows/cols() to initialize
110  * `maxsize'. Since video resolution changes are less common and more obtrusive
111  * than changes to terminal size, the dialog_maxrows/cols() functions only call
112  * this function when `maxsize' is set to NULL.
113  */
114 static void
115 x11_maxsize_update(void)
116 {
117 	FILE *f = NULL;
118 	char *cols;
119 	char *cp;
120 	char *rows;
121 	char cmdbuf[LINE_MAX];
122 	char rbuf[LINE_MAX];
123 
124 	if (maxsize == NULL) {
125 		if ((maxsize = malloc(sizeof(struct winsize))) == NULL)
126 			errx(EXIT_FAILURE, "Out of memory?!");
127 		memset((void *)maxsize, '\0', sizeof(struct winsize));
128 	}
129 
130 	/* Assemble the command necessary to get X11 sizes */
131 	snprintf(cmdbuf, LINE_MAX, "%s --print-maxsize 2>&1", dialog);
132 
133 	fflush(STDIN_FILENO); /* prevent popen(3) from seeking on stdin */
134 
135 	if ((f = popen(cmdbuf, "r")) == NULL) {
136 		if (debug)
137 			warnx("WARNING! Command `%s' failed", cmdbuf);
138 		return;
139 	}
140 
141 	/* Read in the line returned from Xdialog(1) */
142 	if ((fgets(rbuf, LINE_MAX, f) == NULL) || (pclose(f) < 0))
143 		return;
144 
145 	/* Check for X11-related errors */
146 	if (strncmp(rbuf, "Xdialog: Error", 14) == 0)
147 		return;
148 
149 	/* Parse expected output: MaxSize: YY, XXX */
150 	if ((rows = strchr(rbuf, ' ')) == NULL)
151 		return;
152 	if ((cols = strchr(rows, ',')) != NULL) {
153 		/* strtonum(3) doesn't like trailing junk */
154 		*(cols++) = '\0';
155 		if ((cp = strchr(cols, '\n')) != NULL)
156 			*cp = '\0';
157 	}
158 
159 	/* Convert to unsigned short */
160 	maxsize->ws_row = (unsigned short)strtonum(
161 	    rows, 0, USHRT_MAX, (const char **)NULL);
162 	maxsize->ws_col = (unsigned short)strtonum(
163 	    cols, 0, USHRT_MAX, (const char **)NULL);
164 }
165 
166 /*
167  * Return the current maximum height (rows) for an [X]dialog(1) widget.
168  */
169 int
170 dialog_maxrows(void)
171 {
172 
173 	if (use_xdialog && maxsize == NULL)
174 		x11_maxsize_update(); /* initialize maxsize for GUI */
175 	else if (!use_xdialog)
176 		tty_maxsize_update(); /* update maxsize for TTY */
177 	return (maxsize->ws_row);
178 }
179 
180 /*
181  * Return the current maximum width (cols) for an [X]dialog(1) widget.
182  */
183 int
184 dialog_maxcols(void)
185 {
186 
187 	if (use_xdialog && maxsize == NULL)
188 		x11_maxsize_update(); /* initialize maxsize for GUI */
189 	else if (!use_xdialog)
190 		tty_maxsize_update(); /* update maxsize for TTY */
191 
192 	if (use_dialog || use_libdialog) {
193 		if (use_shadow)
194 			return (maxsize->ws_col - 2);
195 		else
196 			return (maxsize->ws_col);
197 	} else
198 		return (maxsize->ws_col);
199 }
200 
201 /*
202  * Return the current maximum width (cols) for the terminal.
203  */
204 int
205 tty_maxcols(void)
206 {
207 
208 	if (use_xdialog && maxsize == NULL)
209 		x11_maxsize_update(); /* initialize maxsize for GUI */
210 	else if (!use_xdialog)
211 		tty_maxsize_update(); /* update maxsize for TTY */
212 
213 	return (maxsize->ws_col);
214 }
215 
216 /*
217  * Spawn an [X]dialog(1) `--gauge' box with a `--prompt' value of init_prompt.
218  * Writes the resulting process ID to the pid_t pointed at by `pid'. Returns a
219  * file descriptor (int) suitable for writing data to the [X]dialog(1) instance
220  * (data written to the file descriptor is seen as standard-in by the spawned
221  * [X]dialog(1) process).
222  */
223 int
224 dialog_spawn_gauge(char *init_prompt, pid_t *pid)
225 {
226 	char dummy_init[2] = "";
227 	char *cp;
228 	int height;
229 	int width;
230 	int error;
231 	posix_spawn_file_actions_t action;
232 #if DIALOG_SPAWN_DEBUG
233 	unsigned int i;
234 #endif
235 	unsigned int n = 0;
236 	int stdin_pipe[2] = { -1, -1 };
237 
238 	/* Override `dialog' with a path from ENV_DIALOG if provided */
239 	if ((cp = getenv(ENV_DIALOG)) != NULL)
240 		snprintf(dialog, PATH_MAX, "%s", cp);
241 
242 	/* For Xdialog(1), set ENV_XDIALOG_HIGH_DIALOG_COMPAT */
243 	setenv(ENV_XDIALOG_HIGH_DIALOG_COMPAT, "1", 1);
244 
245 	/* Constrain the height/width */
246 	height = dialog_maxrows();
247 	if (backtitle != NULL)
248 		height -= use_shadow ? 5 : 4;
249 	if (dheight < height)
250 		height = dheight;
251 	width = dialog_maxcols();
252 	if (dwidth < width)
253 		width = dwidth;
254 
255 	/* Populate argument array */
256 	dargv[n++] = dialog;
257 	if (title != NULL) {
258 		if ((dargv[n] = malloc(8)) == NULL)
259 			errx(EXIT_FAILURE, "Out of memory?!");
260 		sprintf(dargv[n++], "--title");
261 		dargv[n++] = title;
262 	} else {
263 		if ((dargv[n] = malloc(8)) == NULL)
264 			errx(EXIT_FAILURE, "Out of memory?!");
265 		sprintf(dargv[n++], "--title");
266 		if ((dargv[n] = malloc(1)) == NULL)
267 			errx(EXIT_FAILURE, "Out of memory?!");
268 		*dargv[n++] = '\0';
269 	}
270 	if (backtitle != NULL) {
271 		if ((dargv[n] = malloc(12)) == NULL)
272 			errx(EXIT_FAILURE, "Out of memory?!");
273 		sprintf(dargv[n++], "--backtitle");
274 		dargv[n++] = backtitle;
275 	}
276 	if (use_color) {
277 		if ((dargv[n] = malloc(11)) == NULL)
278 			errx(EXIT_FAILURE, "Out of memory?!");
279 		sprintf(dargv[n++], "--colors");
280 	}
281 	if (use_xdialog) {
282 		if ((dargv[n] = malloc(7)) == NULL)
283 			errx(EXIT_FAILURE, "Out of memory?!");
284 		sprintf(dargv[n++], "--left");
285 
286 		/*
287 		 * NOTE: Xdialog(1)'s `--wrap' appears to be broken for the
288 		 * `--gauge' widget prompt-updates. Add it anyway (in-case it
289 		 * gets fixed in some later release).
290 		 */
291 		if ((dargv[n] = malloc(7)) == NULL)
292 			errx(EXIT_FAILURE, "Out of memory?!");
293 		sprintf(dargv[n++], "--wrap");
294 	}
295 	if ((dargv[n] = malloc(8)) == NULL)
296 		errx(EXIT_FAILURE, "Out of memory?!");
297 	sprintf(dargv[n++], "--gauge");
298 	dargv[n++] = use_xdialog ? dummy_init : init_prompt;
299 	if ((dargv[n] = malloc(40)) == NULL)
300 		errx(EXIT_FAILURE, "Out of memory?!");
301 	snprintf(dargv[n++], 40, "%u", height);
302 	if ((dargv[n] = malloc(40)) == NULL)
303 		errx(EXIT_FAILURE, "Out of memory?!");
304 	snprintf(dargv[n++], 40, "%u", width);
305 	dargv[n] = NULL;
306 
307 	/* Open a pipe(2) to communicate with [X]dialog(1) */
308 	if (pipe(stdin_pipe) < 0)
309 		err(EXIT_FAILURE, "%s: pipe(2)", __func__);
310 
311 	/* Fork [X]dialog(1) process */
312 #if DIALOG_SPAWN_DEBUG
313 	fprintf(stderr, "%s: spawning `", __func__);
314 	for (i = 0; i < n; i++) {
315 		if (i == 0)
316 			fprintf(stderr, "%s", dargv[i]);
317 		else if (*dargv[i] == '-' && *(dargv[i] + 1) == '-')
318 			fprintf(stderr, " %s", dargv[i]);
319 		else
320 			fprintf(stderr, " \"%s\"", dargv[i]);
321 	}
322 	fprintf(stderr, "'\n");
323 #endif
324 	posix_spawn_file_actions_init(&action);
325 	posix_spawn_file_actions_adddup2(&action, stdin_pipe[0], STDIN_FILENO);
326 	posix_spawn_file_actions_addclose(&action, stdin_pipe[1]);
327 	error = posix_spawnp(pid, dialog, &action,
328 	    (const posix_spawnattr_t *)NULL, dargv, environ);
329 	if (error != 0) err(EXIT_FAILURE, "%s", dialog);
330 
331 	/* NB: Do not free(3) *dargv[], else SIGSEGV */
332 
333 	return (stdin_pipe[1]);
334 }
335 
336 /*
337  * Returns the number of lines in buffer pointed to by `prompt'. Takes both
338  * newlines and escaped-newlines into account.
339  */
340 unsigned int
341 dialog_prompt_numlines(const char *prompt, uint8_t nlstate)
342 {
343 	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
344 	const char *cp = prompt;
345 	unsigned int nlines = 1;
346 
347 	if (prompt == NULL || *prompt == '\0')
348 		return (0);
349 
350 	while (*cp != '\0') {
351 		if (use_dialog) {
352 			if (strncmp(cp, "\\n", 2) == 0) {
353 				cp++;
354 				nlines++;
355 				nls = TRUE; /* See declaration comment */
356 			} else if (*cp == '\n') {
357 				if (!nls)
358 					nlines++;
359 				nls = FALSE; /* See declaration comment */
360 			}
361 		} else if (use_libdialog) {
362 			if (*cp == '\n')
363 				nlines++;
364 		} else if (strncmp(cp, "\\n", 2) == 0) {
365 			cp++;
366 			nlines++;
367 		}
368 		cp++;
369 	}
370 
371 	return (nlines);
372 }
373 
374 /*
375  * Returns the length in bytes of the longest line in buffer pointed to by
376  * `prompt'. Takes newlines and escaped newlines into account. Also discounts
377  * dialog(1) color escape codes if enabled (via `use_color' global).
378  */
379 unsigned int
380 dialog_prompt_longestline(const char *prompt, uint8_t nlstate)
381 {
382 	uint8_t backslash = 0;
383 	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
384 	const char *p = prompt;
385 	int longest = 0;
386 	int n = 0;
387 
388 	/* `prompt' parameter is required */
389 	if (prompt == NULL)
390 		return (0);
391 	if (*prompt == '\0')
392 		return (0); /* shortcut */
393 
394 	/* Loop until the end of the string */
395 	while (*p != '\0') {
396 		/* dialog(1) and dialog(3) will render literal newlines */
397 		if (use_dialog || use_libdialog) {
398 			if (*p == '\n') {
399 				if (!use_libdialog && nls)
400 					n++;
401 				else {
402 					if (n > longest)
403 						longest = n;
404 					n = 0;
405 				}
406 				nls = FALSE; /* See declaration comment */
407 				p++;
408 				continue;
409 			}
410 		}
411 
412 		/* Check for backslash character */
413 		if (*p == '\\') {
414 			/* If second backslash, count as a single-char */
415 			if ((backslash ^= 1) == 0)
416 				n++;
417 		} else if (backslash) {
418 			if (*p == 'n' && !use_libdialog) { /* new line */
419 				/* NB: dialog(3) ignores escaped newlines */
420 				nls = TRUE; /* See declaration comment */
421 				if (n > longest)
422 					longest = n;
423 				n = 0;
424 			} else if (use_color && *p == 'Z') {
425 				if (*++p != '\0')
426 					p++;
427 				backslash = 0;
428 				continue;
429 			} else /* [X]dialog(1)/dialog(3) only expand those */
430 				n += 2;
431 
432 			backslash = 0;
433 		} else
434 			n++;
435 		p++;
436 	}
437 	if (n > longest)
438 		longest = n;
439 
440 	return (longest);
441 }
442 
443 /*
444  * Returns a pointer to the last line in buffer pointed to by `prompt'. Takes
445  * both newlines (if using dialog(1) versus Xdialog(1)) and escaped newlines
446  * into account. If no newlines (escaped or otherwise) appear in the buffer,
447  * `prompt' is returned. If passed a NULL pointer, returns NULL.
448  */
449 char *
450 dialog_prompt_lastline(char *prompt, uint8_t nlstate)
451 {
452 	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
453 	char *lastline;
454 	char *p;
455 
456 	if (prompt == NULL)
457 		return (NULL);
458 	if (*prompt == '\0')
459 		return (prompt); /* shortcut */
460 
461 	lastline = p = prompt;
462 	while (*p != '\0') {
463 		/* dialog(1) and dialog(3) will render literal newlines */
464 		if (use_dialog || use_libdialog) {
465 			if (*p == '\n') {
466 				if (use_libdialog || !nls)
467 					lastline = p + 1;
468 				nls = FALSE; /* See declaration comment */
469 			}
470 		}
471 		/* dialog(3) does not expand escaped newlines */
472 		if (use_libdialog) {
473 			p++;
474 			continue;
475 		}
476 		if (*p == '\\' && *(p + 1) != '\0' && *(++p) == 'n') {
477 			nls = TRUE; /* See declaration comment */
478 			lastline = p + 1;
479 		}
480 		p++;
481 	}
482 
483 	return (lastline);
484 }
485 
486 /*
487  * Returns the number of extra lines generated by wrapping the text in buffer
488  * pointed to by `prompt' within `ncols' columns (for prompts, this should be
489  * dwidth - 4). Also discounts dialog(1) color escape codes if enabled (via
490  * `use_color' global).
491  */
492 int
493 dialog_prompt_wrappedlines(char *prompt, int ncols, uint8_t nlstate)
494 {
495 	uint8_t backslash = 0;
496 	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
497 	char *cp;
498 	char *p = prompt;
499 	int n = 0;
500 	int wlines = 0;
501 
502 	/* `prompt' parameter is required */
503 	if (p == NULL)
504 		return (0);
505 	if (*p == '\0')
506 		return (0); /* shortcut */
507 
508 	/* Loop until the end of the string */
509 	while (*p != '\0') {
510 		/* dialog(1) and dialog(3) will render literal newlines */
511 		if (use_dialog || use_libdialog) {
512 			if (*p == '\n') {
513 				if (use_dialog || !nls)
514 					n = 0;
515 				nls = FALSE; /* See declaration comment */
516 			}
517 		}
518 
519 		/* Check for backslash character */
520 		if (*p == '\\') {
521 			/* If second backslash, count as a single-char */
522 			if ((backslash ^= 1) == 0)
523 				n++;
524 		} else if (backslash) {
525 			if (*p == 'n' && !use_libdialog) { /* new line */
526 				/* NB: dialog(3) ignores escaped newlines */
527 				nls = TRUE; /* See declaration comment */
528 				n = 0;
529 			} else if (use_color && *p == 'Z') {
530 				if (*++p != '\0')
531 					p++;
532 				backslash = 0;
533 				continue;
534 			} else /* [X]dialog(1)/dialog(3) only expand those */
535 				n += 2;
536 
537 			backslash = 0;
538 		} else
539 			n++;
540 
541 		/* Did we pass the width barrier? */
542 		if (n > ncols) {
543 			/*
544 			 * Work backward to find the first whitespace on-which
545 			 * dialog(1) will wrap the line (but don't go before
546 			 * the start of this line).
547 			 */
548 			cp = p;
549 			while (n > 1 && !isspace(*cp)) {
550 				cp--;
551 				n--;
552 			}
553 			if (n > 0 && isspace(*cp))
554 				p = cp;
555 			wlines++;
556 			n = 1;
557 		}
558 
559 		p++;
560 	}
561 
562 	return (wlines);
563 }
564 
565 /*
566  * Returns zero if the buffer pointed to by `prompt' contains an escaped
567  * newline but only if appearing after any/all literal newlines. This is
568  * specific to dialog(1) and does not apply to Xdialog(1).
569  *
570  * As an attempt to make shell scripts easier to read, dialog(1) will "eat"
571  * the first literal newline after an escaped newline. This however has a bug
572  * in its implementation in that rather than allowing `\\n\n' to be treated
573  * similar to `\\n' or `\n', dialog(1) expands the `\\n' and then translates
574  * the following literal newline (with or without characters between [!]) into
575  * a single space.
576  *
577  * If you want to be compatible with Xdialog(1), it is suggested that you not
578  * use literal newlines (they aren't supported); but if you have to use them,
579  * go right ahead. But be forewarned... if you set $DIALOG in your environment
580  * to something other than `cdialog' (our current dialog(1)), then it should
581  * do the same thing w/respect to how to handle a literal newline after an
582  * escaped newline (you could do no wrong by translating every literal newline
583  * into a space but only when you've previously encountered an escaped one;
584  * this is what dialog(1) is doing).
585  *
586  * The ``newline state'' (or nlstate for short; as I'm calling it) is helpful
587  * if you plan to combine multiple strings into a single prompt text. In lead-
588  * up to this procedure, a common task is to calculate and utilize the widths
589  * and heights of each piece of prompt text to later be combined. However, if
590  * (for example) the first string ends in a positive newline state (has an
591  * escaped newline without trailing literal), the first literal newline in the
592  * second string will be mangled.
593  *
594  * The return value of this function should be used as the `nlstate' argument
595  * to dialog_*() functions that require it to allow accurate calculations in
596  * the event such information is needed.
597  */
598 uint8_t
599 dialog_prompt_nlstate(const char *prompt)
600 {
601 	const char *cp;
602 
603 	if (prompt == NULL)
604 		return 0;
605 
606 	/*
607 	 * Work our way backward from the end of the string for efficiency.
608 	 */
609 	cp = prompt + strlen(prompt);
610 	while (--cp >= prompt) {
611 		/*
612 		 * If we get to a literal newline first, this prompt ends in a
613 		 * clean state for rendering with dialog(1). Otherwise, if we
614 		 * get to an escaped newline first, this prompt ends in an un-
615 		 * clean state (following literal will be mangled; see above).
616 		 */
617 		if (*cp == '\n')
618 			return (0);
619 		else if (*cp == 'n' && --cp > prompt && *cp == '\\')
620 			return (1);
621 	}
622 
623 	return (0); /* no newlines (escaped or otherwise) */
624 }
625 
626 /*
627  * Free allocated items initialized by tty_maxsize_update() and
628  * x11_maxsize_update()
629  */
630 void
631 dialog_maxsize_free(void)
632 {
633 	if (maxsize != NULL) {
634 		free(maxsize);
635 		maxsize = NULL;
636 	}
637 }
638