]> CyberLeo.Net >> Repos - FreeBSD/stable/10.git/blob - lib/libdpv/dialog_util.c
dpv(3): MFC r330943, r335264
[FreeBSD/stable/10.git] / lib / libdpv / dialog_util.c
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$");
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) 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  */
342 unsigned int
343 dialog_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  */
381 unsigned int
382 dialog_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  */
451 char *
452 dialog_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  */
494 int
495 dialog_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  */
600 uint8_t
601 dialog_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  */
632 void
633 dialog_maxsize_free(void)
634 {
635         if (maxsize != NULL) {
636                 free(maxsize);
637                 maxsize = NULL;
638         }
639 }