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