/*-1* Copyright (c) 2013-2018 Devin Teske <[email protected]>2* All rights reserved.3*4* Redistribution and use in source and binary forms, with or without5* modification, are permitted provided that the following conditions6* are met:7* 1. Redistributions of source code must retain the above copyright8* notice, this list of conditions and the following disclaimer.9* 2. Redistributions in binary form must reproduce the above copyright10* notice, this list of conditions and the following disclaimer in the11* documentation and/or other materials provided with the distribution.12*13* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND14* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE15* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE16* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE17* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL18* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS19* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)20* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT21* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY22* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF23* SUCH DAMAGE.24*/2526#include <sys/cdefs.h>27#include <sys/ioctl.h>2829#include <ctype.h>30#include <err.h>31#include <fcntl.h>32#include <limits.h>33#include <spawn.h>34#include <stdio.h>35#include <stdlib.h>36#include <string.h>37#include <termios.h>38#include <unistd.h>3940#include "dialog_util.h"41#include "dpv.h"42#include "dpv_private.h"4344extern char **environ;4546#define TTY_DEFAULT_ROWS 2447#define TTY_DEFAULT_COLS 804849/* [X]dialog(1) characteristics */50uint8_t dialog_test = 0;51uint8_t use_dialog = 0;52uint8_t use_libdialog = 1;53uint8_t use_xdialog = 0;54uint8_t use_color = 1;55char dialog[PATH_MAX] = DIALOG;5657/* [X]dialog(1) functionality */58char *title = NULL;59char *backtitle = NULL;60int dheight = 0;61int dwidth = 0;62static char *dargv[64] = { NULL };6364/* TTY/Screen characteristics */65static struct winsize *maxsize = NULL;6667/* Function prototypes */68static void tty_maxsize_update(void);69static void x11_maxsize_update(void);7071/*72* Update row/column fields of `maxsize' global (used by dialog_maxrows() and73* dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.74* The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current75* maximum height and width (respectively) for a dialog(1) widget based on the76* active TTY size.77*78* This function is called automatically by dialog_maxrows/cols() to reflect79* changes in terminal size in-between calls.80*/81static void82tty_maxsize_update(void)83{84int fd = STDIN_FILENO;85struct termios t;8687if (maxsize == NULL) {88if ((maxsize = malloc(sizeof(struct winsize))) == NULL)89errx(EXIT_FAILURE, "Out of memory?!");90memset((void *)maxsize, '\0', sizeof(struct winsize));91}9293if (!isatty(fd))94fd = open("/dev/tty", O_RDONLY);95if ((tcgetattr(fd, &t) < 0) || (ioctl(fd, TIOCGWINSZ, maxsize) < 0)) {96maxsize->ws_row = TTY_DEFAULT_ROWS;97maxsize->ws_col = TTY_DEFAULT_COLS;98}99}100101/*102* Update row/column fields of `maxsize' global (used by dialog_maxrows() and103* dialog_maxcols()). If the `maxsize' pointer is NULL, it will be initialized.104* The `ws_row' and `ws_col' fields of `maxsize' are updated to hold current105* maximum height and width (respectively) for an Xdialog(1) widget based on106* the active video resolution of the X11 environment.107*108* This function is called automatically by dialog_maxrows/cols() to initialize109* `maxsize'. Since video resolution changes are less common and more obtrusive110* than changes to terminal size, the dialog_maxrows/cols() functions only call111* this function when `maxsize' is set to NULL.112*/113static void114x11_maxsize_update(void)115{116FILE *f = NULL;117char *cols;118char *cp;119char *rows;120char cmdbuf[LINE_MAX];121char rbuf[LINE_MAX];122123if (maxsize == NULL) {124if ((maxsize = malloc(sizeof(struct winsize))) == NULL)125errx(EXIT_FAILURE, "Out of memory?!");126memset((void *)maxsize, '\0', sizeof(struct winsize));127}128129/* Assemble the command necessary to get X11 sizes */130snprintf(cmdbuf, LINE_MAX, "%s --print-maxsize 2>&1", dialog);131132fflush(STDIN_FILENO); /* prevent popen(3) from seeking on stdin */133134if ((f = popen(cmdbuf, "r")) == NULL) {135if (debug)136warnx("WARNING! Command `%s' failed", cmdbuf);137return;138}139140/* Read in the line returned from Xdialog(1) */141if ((fgets(rbuf, LINE_MAX, f) == NULL) || (pclose(f) < 0))142return;143144/* Check for X11-related errors */145if (strncmp(rbuf, "Xdialog: Error", 14) == 0)146return;147148/* Parse expected output: MaxSize: YY, XXX */149if ((rows = strchr(rbuf, ' ')) == NULL)150return;151if ((cols = strchr(rows, ',')) != NULL) {152/* strtonum(3) doesn't like trailing junk */153*(cols++) = '\0';154if ((cp = strchr(cols, '\n')) != NULL)155*cp = '\0';156}157158/* Convert to unsigned short */159maxsize->ws_row = (unsigned short)strtonum(160rows, 0, USHRT_MAX, (const char **)NULL);161maxsize->ws_col = (unsigned short)strtonum(162cols, 0, USHRT_MAX, (const char **)NULL);163}164165/*166* Return the current maximum height (rows) for an [X]dialog(1) widget.167*/168int169dialog_maxrows(void)170{171172if (use_xdialog && maxsize == NULL)173x11_maxsize_update(); /* initialize maxsize for GUI */174else if (!use_xdialog)175tty_maxsize_update(); /* update maxsize for TTY */176return (maxsize->ws_row);177}178179/*180* Return the current maximum width (cols) for an [X]dialog(1) widget.181*/182int183dialog_maxcols(void)184{185186if (use_xdialog && maxsize == NULL)187x11_maxsize_update(); /* initialize maxsize for GUI */188else if (!use_xdialog)189tty_maxsize_update(); /* update maxsize for TTY */190191if (use_dialog || use_libdialog) {192if (use_shadow)193return (maxsize->ws_col - 2);194else195return (maxsize->ws_col);196} else197return (maxsize->ws_col);198}199200/*201* Return the current maximum width (cols) for the terminal.202*/203int204tty_maxcols(void)205{206207if (use_xdialog && maxsize == NULL)208x11_maxsize_update(); /* initialize maxsize for GUI */209else if (!use_xdialog)210tty_maxsize_update(); /* update maxsize for TTY */211212return (maxsize->ws_col);213}214215/*216* Spawn an [X]dialog(1) `--gauge' box with a `--prompt' value of init_prompt.217* Writes the resulting process ID to the pid_t pointed at by `pid'. Returns a218* file descriptor (int) suitable for writing data to the [X]dialog(1) instance219* (data written to the file descriptor is seen as standard-in by the spawned220* [X]dialog(1) process).221*/222int223dialog_spawn_gauge(char *init_prompt, pid_t *pid)224{225char dummy_init[2] = "";226char *cp;227int height;228int width;229int error;230posix_spawn_file_actions_t action;231#if DIALOG_SPAWN_DEBUG232unsigned int i;233#endif234unsigned int n = 0;235int stdin_pipe[2] = { -1, -1 };236237/* Override `dialog' with a path from ENV_DIALOG if provided */238if ((cp = getenv(ENV_DIALOG)) != NULL)239snprintf(dialog, PATH_MAX, "%s", cp);240241/* For Xdialog(1), set ENV_XDIALOG_HIGH_DIALOG_COMPAT */242setenv(ENV_XDIALOG_HIGH_DIALOG_COMPAT, "1", 1);243244/* Constrain the height/width */245height = dialog_maxrows();246if (backtitle != NULL)247height -= use_shadow ? 5 : 4;248if (dheight < height)249height = dheight;250width = dialog_maxcols();251if (dwidth < width)252width = dwidth;253254/* Populate argument array */255dargv[n++] = dialog;256if (title != NULL) {257if ((dargv[n] = malloc(8)) == NULL)258errx(EXIT_FAILURE, "Out of memory?!");259sprintf(dargv[n++], "--title");260dargv[n++] = title;261} else {262if ((dargv[n] = malloc(8)) == NULL)263errx(EXIT_FAILURE, "Out of memory?!");264sprintf(dargv[n++], "--title");265if ((dargv[n] = malloc(1)) == NULL)266errx(EXIT_FAILURE, "Out of memory?!");267*dargv[n++] = '\0';268}269if (backtitle != NULL) {270if ((dargv[n] = malloc(12)) == NULL)271errx(EXIT_FAILURE, "Out of memory?!");272sprintf(dargv[n++], "--backtitle");273dargv[n++] = backtitle;274}275if (use_color) {276if ((dargv[n] = malloc(11)) == NULL)277errx(EXIT_FAILURE, "Out of memory?!");278sprintf(dargv[n++], "--colors");279}280if (use_xdialog) {281if ((dargv[n] = malloc(7)) == NULL)282errx(EXIT_FAILURE, "Out of memory?!");283sprintf(dargv[n++], "--left");284285/*286* NOTE: Xdialog(1)'s `--wrap' appears to be broken for the287* `--gauge' widget prompt-updates. Add it anyway (in-case it288* gets fixed in some later release).289*/290if ((dargv[n] = malloc(7)) == NULL)291errx(EXIT_FAILURE, "Out of memory?!");292sprintf(dargv[n++], "--wrap");293}294if ((dargv[n] = malloc(8)) == NULL)295errx(EXIT_FAILURE, "Out of memory?!");296sprintf(dargv[n++], "--gauge");297dargv[n++] = use_xdialog ? dummy_init : init_prompt;298if ((dargv[n] = malloc(40)) == NULL)299errx(EXIT_FAILURE, "Out of memory?!");300snprintf(dargv[n++], 40, "%u", height);301if ((dargv[n] = malloc(40)) == NULL)302errx(EXIT_FAILURE, "Out of memory?!");303snprintf(dargv[n++], 40, "%u", width);304dargv[n] = NULL;305306/* Open a pipe(2) to communicate with [X]dialog(1) */307if (pipe(stdin_pipe) < 0)308err(EXIT_FAILURE, "%s: pipe(2)", __func__);309310/* Fork [X]dialog(1) process */311#if DIALOG_SPAWN_DEBUG312fprintf(stderr, "%s: spawning `", __func__);313for (i = 0; i < n; i++) {314if (i == 0)315fprintf(stderr, "%s", dargv[i]);316else if (*dargv[i] == '-' && *(dargv[i] + 1) == '-')317fprintf(stderr, " %s", dargv[i]);318else319fprintf(stderr, " \"%s\"", dargv[i]);320}321fprintf(stderr, "'\n");322#endif323posix_spawn_file_actions_init(&action);324posix_spawn_file_actions_adddup2(&action, stdin_pipe[0], STDIN_FILENO);325posix_spawn_file_actions_addclose(&action, stdin_pipe[1]);326error = posix_spawnp(pid, dialog, &action,327(const posix_spawnattr_t *)NULL, dargv, environ);328if (error != 0) err(EXIT_FAILURE, "%s", dialog);329330/* NB: Do not free(3) *dargv[], else SIGSEGV */331332return (stdin_pipe[1]);333}334335/*336* Returns the number of lines in buffer pointed to by `prompt'. Takes both337* newlines and escaped-newlines into account.338*/339unsigned int340dialog_prompt_numlines(const char *prompt, uint8_t nlstate)341{342uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */343const char *cp = prompt;344unsigned int nlines = 1;345346if (prompt == NULL || *prompt == '\0')347return (0);348349while (*cp != '\0') {350if (use_dialog) {351if (strncmp(cp, "\\n", 2) == 0) {352cp++;353nlines++;354nls = TRUE; /* See declaration comment */355} else if (*cp == '\n') {356if (!nls)357nlines++;358nls = FALSE; /* See declaration comment */359}360} else if (use_libdialog) {361if (*cp == '\n')362nlines++;363} else if (strncmp(cp, "\\n", 2) == 0) {364cp++;365nlines++;366}367cp++;368}369370return (nlines);371}372373/*374* Returns the length in bytes of the longest line in buffer pointed to by375* `prompt'. Takes newlines and escaped newlines into account. Also discounts376* dialog(1) color escape codes if enabled (via `use_color' global).377*/378unsigned int379dialog_prompt_longestline(const char *prompt, uint8_t nlstate)380{381uint8_t backslash = 0;382uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */383const char *p = prompt;384int longest = 0;385int n = 0;386387/* `prompt' parameter is required */388if (prompt == NULL)389return (0);390if (*prompt == '\0')391return (0); /* shortcut */392393/* Loop until the end of the string */394while (*p != '\0') {395/* dialog(1) and dialog(3) will render literal newlines */396if (use_dialog || use_libdialog) {397if (*p == '\n') {398if (!use_libdialog && nls)399n++;400else {401if (n > longest)402longest = n;403n = 0;404}405nls = FALSE; /* See declaration comment */406p++;407continue;408}409}410411/* Check for backslash character */412if (*p == '\\') {413/* If second backslash, count as a single-char */414if ((backslash ^= 1) == 0)415n++;416} else if (backslash) {417if (*p == 'n' && !use_libdialog) { /* new line */418/* NB: dialog(3) ignores escaped newlines */419nls = TRUE; /* See declaration comment */420if (n > longest)421longest = n;422n = 0;423} else if (use_color && *p == 'Z') {424if (*++p != '\0')425p++;426backslash = 0;427continue;428} else /* [X]dialog(1)/dialog(3) only expand those */429n += 2;430431backslash = 0;432} else433n++;434p++;435}436if (n > longest)437longest = n;438439return (longest);440}441442/*443* Returns a pointer to the last line in buffer pointed to by `prompt'. Takes444* both newlines (if using dialog(1) versus Xdialog(1)) and escaped newlines445* into account. If no newlines (escaped or otherwise) appear in the buffer,446* `prompt' is returned. If passed a NULL pointer, returns NULL.447*/448char *449dialog_prompt_lastline(char *prompt, uint8_t nlstate)450{451uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */452char *lastline;453char *p;454455if (prompt == NULL)456return (NULL);457if (*prompt == '\0')458return (prompt); /* shortcut */459460lastline = p = prompt;461while (*p != '\0') {462/* dialog(1) and dialog(3) will render literal newlines */463if (use_dialog || use_libdialog) {464if (*p == '\n') {465if (use_libdialog || !nls)466lastline = p + 1;467nls = FALSE; /* See declaration comment */468}469}470/* dialog(3) does not expand escaped newlines */471if (use_libdialog) {472p++;473continue;474}475if (*p == '\\' && *(p + 1) != '\0' && *(++p) == 'n') {476nls = TRUE; /* See declaration comment */477lastline = p + 1;478}479p++;480}481482return (lastline);483}484485/*486* Returns the number of extra lines generated by wrapping the text in buffer487* pointed to by `prompt' within `ncols' columns (for prompts, this should be488* dwidth - 4). Also discounts dialog(1) color escape codes if enabled (via489* `use_color' global).490*/491int492dialog_prompt_wrappedlines(char *prompt, int ncols, uint8_t nlstate)493{494uint8_t backslash = 0;495uint8_t nls = nlstate; /* See dialog_prompt_nlstate() */496char *cp;497char *p = prompt;498int n = 0;499int wlines = 0;500501/* `prompt' parameter is required */502if (p == NULL)503return (0);504if (*p == '\0')505return (0); /* shortcut */506507/* Loop until the end of the string */508while (*p != '\0') {509/* dialog(1) and dialog(3) will render literal newlines */510if (use_dialog || use_libdialog) {511if (*p == '\n') {512if (use_dialog || !nls)513n = 0;514nls = FALSE; /* See declaration comment */515}516}517518/* Check for backslash character */519if (*p == '\\') {520/* If second backslash, count as a single-char */521if ((backslash ^= 1) == 0)522n++;523} else if (backslash) {524if (*p == 'n' && !use_libdialog) { /* new line */525/* NB: dialog(3) ignores escaped newlines */526nls = TRUE; /* See declaration comment */527n = 0;528} else if (use_color && *p == 'Z') {529if (*++p != '\0')530p++;531backslash = 0;532continue;533} else /* [X]dialog(1)/dialog(3) only expand those */534n += 2;535536backslash = 0;537} else538n++;539540/* Did we pass the width barrier? */541if (n > ncols) {542/*543* Work backward to find the first whitespace on-which544* dialog(1) will wrap the line (but don't go before545* the start of this line).546*/547cp = p;548while (n > 1 && !isspace(*cp)) {549cp--;550n--;551}552if (n > 0 && isspace(*cp))553p = cp;554wlines++;555n = 1;556}557558p++;559}560561return (wlines);562}563564/*565* Returns zero if the buffer pointed to by `prompt' contains an escaped566* newline but only if appearing after any/all literal newlines. This is567* specific to dialog(1) and does not apply to Xdialog(1).568*569* As an attempt to make shell scripts easier to read, dialog(1) will "eat"570* the first literal newline after an escaped newline. This however has a bug571* in its implementation in that rather than allowing `\\n\n' to be treated572* similar to `\\n' or `\n', dialog(1) expands the `\\n' and then translates573* the following literal newline (with or without characters between [!]) into574* a single space.575*576* If you want to be compatible with Xdialog(1), it is suggested that you not577* use literal newlines (they aren't supported); but if you have to use them,578* go right ahead. But be forewarned... if you set $DIALOG in your environment579* to something other than `cdialog' (our current dialog(1)), then it should580* do the same thing w/respect to how to handle a literal newline after an581* escaped newline (you could do no wrong by translating every literal newline582* into a space but only when you've previously encountered an escaped one;583* this is what dialog(1) is doing).584*585* The ``newline state'' (or nlstate for short; as I'm calling it) is helpful586* if you plan to combine multiple strings into a single prompt text. In lead-587* up to this procedure, a common task is to calculate and utilize the widths588* and heights of each piece of prompt text to later be combined. However, if589* (for example) the first string ends in a positive newline state (has an590* escaped newline without trailing literal), the first literal newline in the591* second string will be mangled.592*593* The return value of this function should be used as the `nlstate' argument594* to dialog_*() functions that require it to allow accurate calculations in595* the event such information is needed.596*/597uint8_t598dialog_prompt_nlstate(const char *prompt)599{600const char *cp;601602if (prompt == NULL)603return 0;604605/*606* Work our way backward from the end of the string for efficiency.607*/608cp = prompt + strlen(prompt);609while (--cp >= prompt) {610/*611* If we get to a literal newline first, this prompt ends in a612* clean state for rendering with dialog(1). Otherwise, if we613* get to an escaped newline first, this prompt ends in an un-614* clean state (following literal will be mangled; see above).615*/616if (*cp == '\n')617return (0);618else if (*cp == 'n' && --cp > prompt && *cp == '\\')619return (1);620}621622return (0); /* no newlines (escaped or otherwise) */623}624625/*626* Free allocated items initialized by tty_maxsize_update() and627* x11_maxsize_update()628*/629void630dialog_maxsize_free(void)631{632if (maxsize != NULL) {633free(maxsize);634maxsize = NULL;635}636}637638639