dialog_util.c revision 335406
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__FBSDID("$FreeBSD: stable/11/lib/libdpv/dialog_util.c 335406 2018-06-20 05:45:41Z dteske $");
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
47extern char **environ;
48
49#define TTY_DEFAULT_ROWS	24
50#define TTY_DEFAULT_COLS	80
51
52/* [X]dialog(1) characteristics */
53uint8_t dialog_test	= 0;
54uint8_t use_dialog	= 0;
55uint8_t use_libdialog	= 1;
56uint8_t use_xdialog	= 0;
57uint8_t use_color	= 1;
58char dialog[PATH_MAX]	= DIALOG;
59
60/* [X]dialog(1) functionality */
61char *title	= NULL;
62char *backtitle	= NULL;
63int dheight	= 0;
64int dwidth	= 0;
65static char *dargv[64] = { NULL };
66
67/* TTY/Screen characteristics */
68static struct winsize *maxsize = NULL;
69
70/* Function prototypes */
71static void tty_maxsize_update(void);
72static 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 */
84static void
85tty_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 */
116static void
117x11_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 */
171int
172dialog_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 */
185int
186dialog_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 */
206int
207tty_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 */
225int
226dialog_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) err(EXIT_FAILURE, "%s", dialog);
332
333	/* NB: Do not free(3) *dargv[], else SIGSEGV */
334
335	return (stdin_pipe[1]);
336}
337
338/*
339 * Returns the number of lines in buffer pointed to by `prompt'. Takes both
340 * newlines and escaped-newlines into account.
341 */
342unsigned int
343dialog_prompt_numlines(const char *prompt, uint8_t nlstate)
344{
345	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
346	const char *cp = prompt;
347	unsigned int nlines = 1;
348
349	if (prompt == NULL || *prompt == '\0')
350		return (0);
351
352	while (*cp != '\0') {
353		if (use_dialog) {
354			if (strncmp(cp, "\\n", 2) == 0) {
355				cp++;
356				nlines++;
357				nls = TRUE; /* See declaration comment */
358			} else if (*cp == '\n') {
359				if (!nls)
360					nlines++;
361				nls = FALSE; /* See declaration comment */
362			}
363		} else if (use_libdialog) {
364			if (*cp == '\n')
365				nlines++;
366		} else if (strncmp(cp, "\\n", 2) == 0) {
367			cp++;
368			nlines++;
369		}
370		cp++;
371	}
372
373	return (nlines);
374}
375
376/*
377 * Returns the length in bytes of the longest line in buffer pointed to by
378 * `prompt'. Takes newlines and escaped newlines into account. Also discounts
379 * dialog(1) color escape codes if enabled (via `use_color' global).
380 */
381unsigned int
382dialog_prompt_longestline(const char *prompt, uint8_t nlstate)
383{
384	uint8_t backslash = 0;
385	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
386	const char *p = prompt;
387	int longest = 0;
388	int n = 0;
389
390	/* `prompt' parameter is required */
391	if (prompt == NULL)
392		return (0);
393	if (*prompt == '\0')
394		return (0); /* shortcut */
395
396	/* Loop until the end of the string */
397	while (*p != '\0') {
398		/* dialog(1) and dialog(3) will render literal newlines */
399		if (use_dialog || use_libdialog) {
400			if (*p == '\n') {
401				if (!use_libdialog && nls)
402					n++;
403				else {
404					if (n > longest)
405						longest = n;
406					n = 0;
407				}
408				nls = FALSE; /* See declaration comment */
409				p++;
410				continue;
411			}
412		}
413
414		/* Check for backslash character */
415		if (*p == '\\') {
416			/* If second backslash, count as a single-char */
417			if ((backslash ^= 1) == 0)
418				n++;
419		} else if (backslash) {
420			if (*p == 'n' && !use_libdialog) { /* new line */
421				/* NB: dialog(3) ignores escaped newlines */
422				nls = TRUE; /* See declaration comment */
423				if (n > longest)
424					longest = n;
425				n = 0;
426			} else if (use_color && *p == 'Z') {
427				if (*++p != '\0')
428					p++;
429				backslash = 0;
430				continue;
431			} else /* [X]dialog(1)/dialog(3) only expand those */
432				n += 2;
433
434			backslash = 0;
435		} else
436			n++;
437		p++;
438	}
439	if (n > longest)
440		longest = n;
441
442	return (longest);
443}
444
445/*
446 * Returns a pointer to the last line in buffer pointed to by `prompt'. Takes
447 * both newlines (if using dialog(1) versus Xdialog(1)) and escaped newlines
448 * into account. If no newlines (escaped or otherwise) appear in the buffer,
449 * `prompt' is returned. If passed a NULL pointer, returns NULL.
450 */
451char *
452dialog_prompt_lastline(char *prompt, uint8_t nlstate)
453{
454	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
455	char *lastline;
456	char *p;
457
458	if (prompt == NULL)
459		return (NULL);
460	if (*prompt == '\0')
461		return (prompt); /* shortcut */
462
463	lastline = p = prompt;
464	while (*p != '\0') {
465		/* dialog(1) and dialog(3) will render literal newlines */
466		if (use_dialog || use_libdialog) {
467			if (*p == '\n') {
468				if (use_libdialog || !nls)
469					lastline = p + 1;
470				nls = FALSE; /* See declaration comment */
471			}
472		}
473		/* dialog(3) does not expand escaped newlines */
474		if (use_libdialog) {
475			p++;
476			continue;
477		}
478		if (*p == '\\' && *(p + 1) != '\0' && *(++p) == 'n') {
479			nls = TRUE; /* See declaration comment */
480			lastline = p + 1;
481		}
482		p++;
483	}
484
485	return (lastline);
486}
487
488/*
489 * Returns the number of extra lines generated by wrapping the text in buffer
490 * pointed to by `prompt' within `ncols' columns (for prompts, this should be
491 * dwidth - 4). Also discounts dialog(1) color escape codes if enabled (via
492 * `use_color' global).
493 */
494int
495dialog_prompt_wrappedlines(char *prompt, int ncols, uint8_t nlstate)
496{
497	uint8_t backslash = 0;
498	uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */
499	char *cp;
500	char *p = prompt;
501	int n = 0;
502	int wlines = 0;
503
504	/* `prompt' parameter is required */
505	if (p == NULL)
506		return (0);
507	if (*p == '\0')
508		return (0); /* shortcut */
509
510	/* Loop until the end of the string */
511	while (*p != '\0') {
512		/* dialog(1) and dialog(3) will render literal newlines */
513		if (use_dialog || use_libdialog) {
514			if (*p == '\n') {
515				if (use_dialog || !nls)
516					n = 0;
517				nls = FALSE; /* See declaration comment */
518			}
519		}
520
521		/* Check for backslash character */
522		if (*p == '\\') {
523			/* If second backslash, count as a single-char */
524			if ((backslash ^= 1) == 0)
525				n++;
526		} else if (backslash) {
527			if (*p == 'n' && !use_libdialog) { /* new line */
528				/* NB: dialog(3) ignores escaped newlines */
529				nls = TRUE; /* See declaration comment */
530				n = 0;
531			} else if (use_color && *p == 'Z') {
532				if (*++p != '\0')
533					p++;
534				backslash = 0;
535				continue;
536			} else /* [X]dialog(1)/dialog(3) only expand those */
537				n += 2;
538
539			backslash = 0;
540		} else
541			n++;
542
543		/* Did we pass the width barrier? */
544		if (n > ncols) {
545			/*
546			 * Work backward to find the first whitespace on-which
547			 * dialog(1) will wrap the line (but don't go before
548			 * the start of this line).
549			 */
550			cp = p;
551			while (n > 1 && !isspace(*cp)) {
552				cp--;
553				n--;
554			}
555			if (n > 0 && isspace(*cp))
556				p = cp;
557			wlines++;
558			n = 1;
559		}
560
561		p++;
562	}
563
564	return (wlines);
565}
566
567/*
568 * Returns zero if the buffer pointed to by `prompt' contains an escaped
569 * newline but only if appearing after any/all literal newlines. This is
570 * specific to dialog(1) and does not apply to Xdialog(1).
571 *
572 * As an attempt to make shell scripts easier to read, dialog(1) will "eat"
573 * the first literal newline after an escaped newline. This however has a bug
574 * in its implementation in that rather than allowing `\\n\n' to be treated
575 * similar to `\\n' or `\n', dialog(1) expands the `\\n' and then translates
576 * the following literal newline (with or without characters between [!]) into
577 * a single space.
578 *
579 * If you want to be compatible with Xdialog(1), it is suggested that you not
580 * use literal newlines (they aren't supported); but if you have to use them,
581 * go right ahead. But be forewarned... if you set $DIALOG in your environment
582 * to something other than `cdialog' (our current dialog(1)), then it should
583 * do the same thing w/respect to how to handle a literal newline after an
584 * escaped newline (you could do no wrong by translating every literal newline
585 * into a space but only when you've previously encountered an escaped one;
586 * this is what dialog(1) is doing).
587 *
588 * The ``newline state'' (or nlstate for short; as I'm calling it) is helpful
589 * if you plan to combine multiple strings into a single prompt text. In lead-
590 * up to this procedure, a common task is to calculate and utilize the widths
591 * and heights of each piece of prompt text to later be combined. However, if
592 * (for example) the first string ends in a positive newline state (has an
593 * escaped newline without trailing literal), the first literal newline in the
594 * second string will be mangled.
595 *
596 * The return value of this function should be used as the `nlstate' argument
597 * to dialog_*() functions that require it to allow accurate calculations in
598 * the event such information is needed.
599 */
600uint8_t
601dialog_prompt_nlstate(const char *prompt)
602{
603	const char *cp;
604
605	if (prompt == NULL)
606		return 0;
607
608	/*
609	 * Work our way backward from the end of the string for efficiency.
610	 */
611	cp = prompt + strlen(prompt);
612	while (--cp >= prompt) {
613		/*
614		 * If we get to a literal newline first, this prompt ends in a
615		 * clean state for rendering with dialog(1). Otherwise, if we
616		 * get to an escaped newline first, this prompt ends in an un-
617		 * clean state (following literal will be mangled; see above).
618		 */
619		if (*cp == '\n')
620			return (0);
621		else if (*cp == 'n' && --cp > prompt && *cp == '\\')
622			return (1);
623	}
624
625	return (0); /* no newlines (escaped or otherwise) */
626}
627
628/*
629 * Free allocated items initialized by tty_maxsize_update() and
630 * x11_maxsize_update()
631 */
632void
633dialog_maxsize_free(void)
634{
635	if (maxsize != NULL) {
636		free(maxsize);
637		maxsize = NULL;
638	}
639}
640