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