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