2 * Copyright (c) 2013-2014 Devin Teske <dteske@FreeBSD.org>
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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
27 #include <sys/cdefs.h>
28 #include <sys/types.h>
30 #define _BSD_SOURCE /* to get dprintf() prototype in stdio.h below */
41 #include "dialog_util.h"
45 #include "dpv_private.h"
47 #define FLABEL_MAX 1024
49 static int fheight = 0; /* initialized by dprompt_init() */
50 static char dprompt[PROMPT_MAX + 1] = "";
51 static char *dprompt_pos = (char *)(0); /* treated numerically */
53 /* Display characteristics */
57 static uint8_t dprompt_free_mask;
58 static char *done = NULL;
59 static char *fail = NULL;
60 static char *pend = NULL;
61 int display_limit = DISPLAY_LIMIT_DEFAULT; /* Max entries to show */
62 int label_size = LABEL_SIZE_DEFAULT; /* Max width for labels */
63 int pbar_size = PBAR_SIZE_DEFAULT; /* Mini-progressbar size */
64 static int gauge_percent = 0;
65 static int done_size, done_lsize, done_rsize;
66 static int fail_size, fail_lsize, fail_rsize;
67 static int mesg_size, mesg_lsize, mesg_rsize;
68 static int pend_size, pend_lsize, pend_rsize;
69 static int pct_lsize, pct_rsize;
70 static void *gauge = NULL;
72 static char spin[SPIN_SIZE + 1] = "/-\\|";
73 static char msg[PROMPT_MAX + 1];
74 static char *spin_cp = spin;
76 /* Function prototypes */
77 static char spin_char(void);
78 static int dprompt_add_files(struct dpv_file_node *file_list,
79 struct dpv_file_node *curfile, int pct);
82 * Returns a pointer to the current spin character in the spin string and
83 * advances the global position to the next character for the next call.
94 /* Advance the spinner to the next char */
95 if (++spin_cp >= (spin + SPIN_SIZE))
102 * Initialize heights and widths based on various strings and environment
103 * variables (such as ENV_USE_COLOR).
106 dprompt_init(struct dpv_file_node *file_list)
114 struct dpv_file_node *curfile;
117 * Initialize dialog(3) `colors' support and draw backtitle
119 if (use_libdialog && !debug) {
120 init_dialog(stdin, stdout);
121 dialog_vars.colors = 1;
122 if (backtitle != NULL) {
123 dialog_vars.backtitle = (char *)backtitle;
128 /* Calculate width of dialog(3) or [X]dialog(1) --gauge box */
129 dwidth = label_size + pbar_size + 9;
132 * Calculate height of dialog(3) or [X]dialog(1) --gauge box
135 max_rows = dialog_maxrows();
136 /* adjust max_rows for backtitle and/or dialog(3) statusLine */
137 if (backtitle != NULL)
138 max_rows -= use_shadow ? 3 : 2;
139 if (use_libdialog && use_shadow)
141 /* add lines for `-p text' */
142 numlines = dialog_prompt_numlines(pprompt, 0);
144 warnx("`-p text' is %i line%s long", numlines,
145 numlines == 1 ? "" : "s");
147 /* adjust dheight for various implementations */
149 dheight -= dialog_prompt_nlstate(pprompt);
150 nls = dialog_prompt_nlstate(pprompt);
151 } else if (use_xdialog) {
152 if (pprompt == NULL || *pprompt == '\0')
154 } else if (use_libdialog) {
155 if (pprompt != NULL && *pprompt != '\0')
158 /* limit the number of display items (necessary per dialog(1,3)) */
159 if (display_limit == 0 || display_limit > DPV_DISPLAY_LIMIT)
160 display_limit = DPV_DISPLAY_LIMIT;
161 /* verify fheight will fit (stop if we hit 1) */
162 for (; display_limit > 0; display_limit--) {
163 nthfile = numlines = 0;
164 fheight = (int)dpv_nfiles > display_limit ?
165 (unsigned int)display_limit : dpv_nfiles;
166 for (curfile = file_list; curfile != NULL;
167 curfile = curfile->next) {
169 numlines += dialog_prompt_numlines(curfile->name, nls);
170 if ((nthfile % display_limit) == 0) {
171 if (numlines > fheight)
173 numlines = nthfile = 0;
176 if (numlines > fheight)
178 if ((dheight + fheight +
179 (int)dialog_prompt_numlines(aprompt, use_dialog) -
180 (use_dialog ? (int)dialog_prompt_nlstate(aprompt) : 0))
184 /* don't show any items if we run the risk of hitting a blank set */
185 if ((max_rows - (use_shadow ? 5 : 4)) >= fheight)
189 /* add lines for `-a text' */
190 numlines = dialog_prompt_numlines(aprompt, use_dialog);
192 warnx("`-a text' is %i line%s long", numlines,
193 numlines == 1 ? "" : "s");
196 /* If using Xdialog(1), adjust accordingly (based on testing) */
198 dheight += dheight / 4;
200 /* For wide mode, long prefix (`pprompt') or append (`aprompt')
201 * strings will bump width */
203 len = (int)dialog_prompt_longestline(pprompt, 0); /* !nls */
204 if ((len + 4) > dwidth)
206 len = (int)dialog_prompt_longestline(aprompt, 1); /* nls */
207 if ((len + 4) > dwidth)
211 /* Enforce width constraints to maximum values */
212 max_cols = dialog_maxcols();
213 if (max_cols > 0 && dwidth > max_cols)
216 /* Optimize widths to sane values*/
217 if (pbar_size > dwidth - 9) {
218 pbar_size = dwidth - 9;
220 /* -9 = "| - [" ... "] |" */
223 label_size = dwidth - 8;
224 /* -8 = "| " ... " - |" */
225 else if (label_size > (dwidth - pbar_size - 9) || wide)
226 label_size = no_labels ? 0 : dwidth - pbar_size - 9;
227 /* -9 = "| " ... " - [" ... "] |" */
229 /* Hide labels if requested */
233 /* Touch up the height (now that we know dwidth) */
234 dheight += dialog_prompt_wrappedlines(pprompt, dwidth - 4, 0);
235 dheight += dialog_prompt_wrappedlines(aprompt, dwidth - 4, 1);
238 warnx("dheight = %i dwidth = %i fheight = %i",
239 dheight, dwidth, fheight);
241 /* Calculate left/right portions of % */
242 pct_lsize = (pbar_size - 4) / 2; /* -4 == printf("%-3s%%", pct) */
243 pct_rsize = pct_lsize;
244 /* If not evenly divisible by 2, increment the right-side */
245 if ((pct_rsize + pct_rsize + 4) != pbar_size)
248 /* Initialize "Done" text */
249 if (done == NULL && (done = msg_done) == NULL) {
250 if ((done = getenv(ENV_MSG_DONE)) != NULL)
251 done_size = strlen(done);
253 done_size = strlen(DPV_DONE_DEFAULT);
254 if ((done = malloc(done_size + 1)) == NULL)
255 errx(EXIT_FAILURE, "Out of memory?!");
256 dprompt_free_mask |= FM_DONE;
257 snprintf(done, done_size + 1, DPV_DONE_DEFAULT);
260 if (pbar_size < done_size) {
261 done_lsize = done_rsize = 0;
262 *(done + pbar_size) = '\0';
263 done_size = pbar_size;
265 /* Calculate left/right portions for mini-progressbar */
266 done_lsize = (pbar_size - done_size) / 2;
267 done_rsize = done_lsize;
268 /* If not evenly divisible by 2, increment the right-side */
269 if ((done_rsize + done_size + done_lsize) != pbar_size)
273 /* Initialize "Fail" text */
274 if (fail == NULL && (fail = msg_fail) == NULL) {
275 if ((fail = getenv(ENV_MSG_FAIL)) != NULL)
276 fail_size = strlen(fail);
278 fail_size = strlen(DPV_FAIL_DEFAULT);
279 if ((fail = malloc(fail_size + 1)) == NULL)
280 errx(EXIT_FAILURE, "Out of memory?!");
281 dprompt_free_mask |= FM_FAIL;
282 snprintf(fail, fail_size + 1, DPV_FAIL_DEFAULT);
285 if (pbar_size < fail_size) {
286 fail_lsize = fail_rsize = 0;
287 *(fail + pbar_size) = '\0';
288 fail_size = pbar_size;
290 /* Calculate left/right portions for mini-progressbar */
291 fail_lsize = (pbar_size - fail_size) / 2;
292 fail_rsize = fail_lsize;
293 /* If not evenly divisible by 2, increment the right-side */
294 if ((fail_rsize + fail_size + fail_lsize) != pbar_size)
298 /* Initialize "Pending" text */
299 if (pend == NULL && (pend = msg_pending) == NULL) {
300 if ((pend = getenv(ENV_MSG_PENDING)) != NULL)
301 pend_size = strlen(pend);
303 pend_size = strlen(DPV_PENDING_DEFAULT);
304 if ((pend = malloc(pend_size + 1)) == NULL)
305 errx(EXIT_FAILURE, "Out of memory?!");
306 dprompt_free_mask |= FM_PEND;
307 snprintf(pend, pend_size + 1, DPV_PENDING_DEFAULT);
310 if (pbar_size < pend_size) {
311 pend_lsize = pend_rsize = 0;
312 *(pend + pbar_size) = '\0';
313 pend_size = pbar_size;
315 /* Calculate left/right portions for mini-progressbar */
316 pend_lsize = (pbar_size - pend_size) / 2;
317 pend_rsize = pend_lsize;
318 /* If not evenly divisible by 2, increment the right-side */
319 if ((pend_rsize + pend_lsize + pend_size) != pbar_size)
324 warnx("label_size = %i pbar_size = %i", label_size, pbar_size);
330 * Clear the [X]dialog(1) `--gauge' prompt buffer.
337 dprompt_pos = dprompt;
341 * Append to the [X]dialog(1) `--gauge' prompt buffer. Syntax is like printf(3)
342 * and returns the number of bytes appended to the buffer.
345 dprompt_add(const char *format, ...)
350 if (dprompt_pos >= (dprompt + PROMPT_MAX))
353 va_start(ap, format);
354 len = vsnprintf(dprompt_pos, (size_t)(PROMPT_MAX -
355 (dprompt_pos - dprompt)), format, ap);
358 errx(EXIT_FAILURE, "%s: Oops, dprompt buffer overflow",
361 if ((dprompt_pos + len) < (dprompt + PROMPT_MAX))
364 dprompt_pos = dprompt + PROMPT_MAX;
370 * Append active files to the [X]dialog(1) `--gauge' prompt buffer. Syntax
371 * requires a pointer to the head of the dpv_file_node linked-list. Returns the
372 * number of files processed successfully.
375 dprompt_add_files(struct dpv_file_node *file_list,
376 struct dpv_file_node *curfile, int pct)
379 char bold_code = 'b'; /* default: enabled */
380 char color_code = '4'; /* default: blue */
381 uint8_t after_curfile = curfile != NULL ? FALSE : TRUE;
389 enum dprompt_state dstate;
399 struct dpv_file_node *fp;
400 char flabel[FLABEL_MAX + 1];
402 char pbar[pbar_size + 16]; /* +15 for optional color */
403 char pbar_cap[sizeof(pbar)];
404 char pbar_fill[sizeof(pbar)];
407 /* Override color defaults with that of main progress bar */
408 if (use_colors || use_shadow) { /* NB: shadow enables color */
409 color_code = gauge_color[0];
410 /* NB: str[1] aka bg is unused */
411 bold_code = gauge_color[2];
415 * Create mini-progressbar for current file (if applicable)
418 if (pbar_size >= 0 && pct >= 0 && curfile != NULL &&
419 (curfile->length >= 0 || dialog_test)) {
420 snprintf(pbar, pbar_size + 1, "%*s%3u%%%*s", pct_lsize, "",
423 /* Calculate the fill-width of progressbar */
424 pwidth = pct * pbar_size / 100;
425 /* Round up based on one-tenth of a percent */
426 if ((pct * pbar_size % 100) > 50)
430 * Make two copies of pbar. Make one represent the fill
431 * and the other the remainder (cap). We'll insert the
432 * ANSI delimiter in between.
436 strncat(pbar_fill, (const char *)(pbar), dwidth);
437 *(pbar_fill + pwidth) = '\0';
438 strncat(pbar_cap, (const char *)(pbar+pwidth), dwidth);
440 /* Finalize the mini [color] progressbar */
441 snprintf(pbar, sizeof(pbar),
442 "\\Z%c\\Zr\\Z%c%s%s%s\\Zn", bold_code, color_code,
443 pbar_fill, "\\ZR", pbar_cap);
447 for (fp = file_list; fp != NULL; fp = fp->next) {
448 flabel_size = label_size;
453 * Support multiline filenames (where the filename is taken as
454 * the last line and the text leading up to the last line can
455 * be used as (for example) a heading/separator between files.
458 nls = dialog_prompt_nlstate(pprompt);
459 nlines += dialog_prompt_numlines(name, nls);
460 lastline = dialog_prompt_lastline(name, 1);
461 if (name != lastline) {
464 dprompt_add("%s", name);
469 /* Support color codes (for dialog(1,3)) in file names */
470 if ((use_dialog || use_libdialog) && use_color) {
472 while (*cp != '\0') {
473 if (*cp == '\\' && *(cp + 1) != '\0' &&
474 *(++cp) == 'Z' && *(cp + 1) != '\0') {
480 if (flabel_size > FLABEL_MAX)
481 flabel_size = FLABEL_MAX;
484 /* If no mini-progressbar, increase label width */
485 if (pbar_size < 0 && flabel_size <= FLABEL_MAX - 2 &&
489 /* If name is too long, add an ellipsis */
490 if (snprintf(flabel, flabel_size + 1, "%s", name) >
491 flabel_size) sprintf(flabel + flabel_size - 3, "...");
494 * Append the label (processing the current file differently)
496 if (fp == curfile && pct < 100) {
498 * Add an ellipsis to current file name if it will fit.
499 * There may be an ellipsis already from truncating the
500 * label (in which case, we already have one).
502 cp = flabel + strlen(flabel);
503 if (cp < (flabel + flabel_size))
504 snprintf(cp, flabel_size -
505 (cp - flabel) + 1, "...");
507 /* Append label (with spinner and optional color) */
508 dprompt_add("%s%-*s%s %c", use_color ? "\\Zb" : "",
509 flabel_size, flabel, use_color ? "\\Zn" : "",
512 dprompt_add("%-*s%s %s", flabel_size,
513 flabel, use_color ? "\\Zn" : "", " ");
516 * Append pbar/status (processing the current file differently)
518 dstate = DPROMPT_NONE;
520 dstate = DPROMPT_CUSTOM_MSG;
521 else if (pbar_size < 0)
522 dstate = DPROMPT_NONE;
523 else if (pbar_size < 4)
524 dstate = DPROMPT_MINIMAL;
525 else if (after_curfile)
526 dstate = DPROMPT_PENDING;
527 else if (fp == curfile) {
530 dstate = DPROMPT_DETAILS;
531 else if (fp->status == DPV_STATUS_RUNNING)
532 dstate = DPROMPT_DETAILS;
534 dstate = DPROMPT_END_STATE;
536 else if (dialog_test) /* status/length ignored */
538 DPROMPT_PBAR : DPROMPT_END_STATE;
539 else if (fp->status == DPV_STATUS_RUNNING)
540 dstate = fp->length < 0 ?
541 DPROMPT_DETAILS : DPROMPT_PBAR;
542 else /* not running */
543 dstate = fp->length < 0 ?
544 DPROMPT_DETAILS : DPROMPT_END_STATE;
545 } else { /* before curfile */
547 dstate = DPROMPT_END_STATE;
549 dstate = fp->length < 0 ?
550 DPROMPT_DETAILS : DPROMPT_END_STATE;
553 " [\\Z%c%s%-*s%s%-*s\\Zn]\\n" :
555 if (fp->status == DPV_STATUS_FAILED) {
556 bg_code = "\\Zr\\Z1"; /* Red */
557 estext_lsize = fail_lsize;
558 estext_rsize = fail_rsize;
560 } else { /* e.g., DPV_STATUS_DONE */
561 bg_code = "\\Zr\\Z2"; /* Green */
562 estext_lsize = done_lsize;
563 estext_rsize = done_rsize;
567 case DPROMPT_PENDING: /* Future file(s) */
568 dprompt_add(" [%-*s%s%-*s]\\n",
569 pend_lsize, "", pend, pend_rsize, "");
571 case DPROMPT_PBAR: /* Current file */
572 dprompt_add(" [%s]\\n", pbar);
574 case DPROMPT_END_STATE: /* Past/Current file(s) */
576 dprompt_add(format, bold_code, bg_code,
577 estext_lsize, "", estext,
581 estext_lsize, "", estext,
584 case DPROMPT_DETAILS: /* Past/Current file(s) */
585 humanize_number(human, pbar_size + 2, fp->read, "",
586 HN_AUTOSCALE, HN_NOSPACE | HN_DIVISOR_1000);
588 /* Calculate center alignment */
589 hlen = (int)strlen(human);
590 lsize = (pbar_size - hlen) / 2;
592 if ((lsize+hlen+rsize) != pbar_size)
596 dprompt_add(format, bold_code, bg_code,
597 lsize, "", human, rsize, "");
600 lsize, "", human, rsize, "");
602 case DPROMPT_CUSTOM_MSG: /* File-specific message override */
603 snprintf(msg, PROMPT_MAX + 1, "%s", fp->msg);
604 if (pbar_size < (mesg_size = strlen(msg))) {
605 mesg_lsize = mesg_rsize = 0;
606 *(msg + pbar_size) = '\0';
607 mesg_size = pbar_size;
609 mesg_lsize = (pbar_size - mesg_size) / 2;
610 mesg_rsize = mesg_lsize;
611 if ((mesg_rsize + mesg_size + mesg_lsize)
616 dprompt_add(format, bold_code, bg_code,
617 mesg_lsize, "", msg, mesg_rsize, "");
619 dprompt_add(format, mesg_lsize, "", msg,
622 case DPROMPT_MINIMAL: /* Short progress bar, minimal room */
624 dprompt_add(format, bold_code, bg_code,
625 pbar_size, "", "", 0, "");
627 dprompt_add(format, pbar_size, "", "", 0, "");
629 case DPROMPT_NONE: /* pbar_size < 0 */
634 * NB: Leading space required for the case when
635 * spin_char() returns a single backslash [\] which
636 * without the space, changes the meaning of `\n'
640 /* Stop building if we've hit the internal limit */
641 if (nthfile >= display_limit)
644 /* If this is the current file, all others are pending */
646 after_curfile = TRUE;
650 * Since we cannot change the height/width of the [X]dialog(1) widget
651 * after spawn, to make things look nice let's pad the height so that
652 * the `-a text' always appears in the same spot.
654 * NOTE: fheight is calculated in dprompt_init(). It represents the
655 * maximum height required to display the set of items (broken up into
656 * pieces of display_limit chunks) whose names contain the most
657 * newlines for any given set.
659 while (nlines < fheight) {
668 * Process the dpv_file_node linked-list of named files, re-generating the
669 * [X]dialog(1) `--gauge' prompt text for the current state of transfers.
672 dprompt_recreate(struct dpv_file_node *file_list,
673 struct dpv_file_node *curfile, int pct)
678 * Re-Build the prompt text
681 if (display_limit > 0)
682 dprompt_add_files(file_list, curfile, pct);
684 /* Xdialog(1) requires newlines (a) escaped and (b) in triplicate */
686 /* Replace `\n' with `\n\\n\n' in dprompt */
687 len = strlen(dprompt);
688 len += strcount(dprompt, "\\n") * 5; /* +5 chars per count */
689 if (len > PROMPT_MAX)
690 errx(EXIT_FAILURE, "%s: Oops, dprompt buffer overflow "
691 "(%zu > %i)", __func__, len, PROMPT_MAX);
692 if (replaceall(dprompt, "\\n", "\n\\n\n") < 0)
693 err(EXIT_FAILURE, "%s: replaceall()", __func__);
695 else if (use_libdialog)
696 strexpandnl(dprompt);
700 * Print the [X]dialog(1) `--gauge' prompt text to a buffer.
703 dprompt_sprint(char * restrict str, const char *prefix, const char *append)
706 return (snprintf(str, PROMPT_MAX, "%s%s%s%s", use_color ? "\\Zn" : "",
707 prefix ? prefix : "", dprompt, append ? append : ""));
711 * Print the [X]dialog(1) `--gauge' prompt text to file descriptor fd (could
712 * be STDOUT_FILENO or a pipe(2) file descriptor to actual [X]dialog(1)).
715 dprompt_dprint(int fd, const char *prefix, const char *append, int overall)
717 int percent = gauge_percent;
719 if (overall >= 0 && overall <= 100)
720 gauge_percent = percent = overall;
721 dprintf(fd, "XXX\n%s%s%s%s\nXXX\n%i\n", use_color ? "\\Zn" : "",
722 prefix ? prefix : "", dprompt, append ? append : "", percent);
727 * Print the dialog(3) `gauge' prompt text using libdialog.
730 dprompt_libprint(const char *prefix, const char *append, int overall)
732 int percent = gauge_percent;
733 char buf[DPV_PPROMPT_MAX + DPV_APROMPT_MAX + DPV_DISPLAY_LIMIT * 1024];
735 dprompt_sprint(buf, prefix, append);
737 if (overall >= 0 && overall <= 100)
738 gauge_percent = percent = overall;
739 gauge = dlg_reallocate_gauge(gauge, title == NULL ? "" : title,
740 buf, dheight, dwidth, percent);
741 dlg_update_gauge(gauge, percent);
745 * Free allocated items initialized by dprompt_init()
750 if ((dprompt_free_mask & FM_DONE) != 0) {
751 dprompt_free_mask ^= FM_DONE;
755 if ((dprompt_free_mask & FM_FAIL) != 0) {
756 dprompt_free_mask ^= FM_FAIL;
760 if ((dprompt_free_mask & FM_PEND) != 0) {
761 dprompt_free_mask ^= FM_PEND;