2 * Copyright (c) 2013-2016 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 __FBSDID("$FreeBSD$");
32 #include <sys/types.h>
46 #include "dialog_util.h"
50 #include "dpv_private.h"
54 /* Test Mechanics (Only used when dpv_config.options |= DPV_TEST_MODE) */
55 #define INCREMENT 1 /* Increment % per-pass test-mode */
56 #define XDIALOG_INCREMENT 15 /* different for slower Xdialog(1) */
57 static uint8_t increment
= INCREMENT
;
60 uint8_t debug
= FALSE
;
63 int dpv_interrupt
= FALSE
;
64 int dpv_abort
= FALSE
;
65 unsigned int dpv_nfiles
= 0;
68 long long dpv_overall_read
= 0;
69 static char pathbuf
[PATH_MAX
];
71 /* Extra display information */
72 uint8_t keep_tite
= FALSE
; /* dpv_config.keep_tite */
73 uint8_t no_labels
= FALSE
; /* dpv_config.options & DPV_NO_LABELS */
74 uint8_t wide
= FALSE
; /* dpv_config.options & DPV_WIDE_MODE */
75 char *aprompt
= NULL
; /* dpv_config.aprompt */
76 char *msg_done
= NULL
; /* dpv_config.msg_done */
77 char *msg_fail
= NULL
; /* dpv_config.msg_fail */
78 char *msg_pending
= NULL
; /* dpv_config.msg_pending */
79 char *pprompt
= NULL
; /* dpv_config.pprompt */
81 /* Status-Line format for when using dialog(3) */
82 static const char *status_format_custom
= NULL
;
83 static char status_format_default
[DPV_STATUS_FORMAT_MAX
];
86 * Takes a pointer to a dpv_config structure containing layout details and
87 * pointer to initial element in a linked-list of dpv_file_node structures,
88 * each presenting a file to process. Executes the `action' function passed-in
89 * as a member to the `config' structure argument.
92 dpv(struct dpv_config
*config
, struct dpv_file_node
*file_list
)
96 uint8_t nls
= FALSE
; /* See dialog_prompt_nlstate() */
97 uint8_t no_overrun
= FALSE
;
98 uint8_t pprompt_nls
= FALSE
; /* See dialog_prompt_nlstate() */
99 uint8_t shrink_label_size
= FALSE
;
107 const char *status_fmt
;
108 const char *path_fmt
;
109 enum dpv_display display_type
;
110 enum dpv_output output_type
;
111 enum dpv_status status
;
112 int (*action
)(struct dpv_file_node
*file
, int out
);
114 int dialog_last_update
= 0;
115 int dialog_old_nthfile
= 0;
116 int dialog_old_seconds
= -1;
117 int dialog_out
= STDOUT_FILENO
;
118 int dialog_update_usec
= 0;
119 int dialog_updates_per_second
;
128 int status_last_update
= 0;
129 int status_old_nthfile
= 0;
130 int status_old_seconds
= -1;
131 int status_update_usec
= 0;
132 int status_updates_per_second
;
136 struct dpv_file_node
*curfile
;
137 struct dpv_file_node
*first_file
;
138 struct dpv_file_node
*list_head
;
140 struct timeval start
;
141 char init_prompt
[PROMPT_MAX
+ 1] = "";
143 /* Initialize globals to default values */
151 dialog_updates_per_second
= DIALOG_UPDATES_PER_SEC
;
152 display_limit
= DISPLAY_LIMIT_DEFAULT
;
153 display_type
= DPV_DISPLAY_LIBDIALOG
;
155 label_size
= LABEL_SIZE_DEFAULT
;
161 output_type
= DPV_OUTPUT_NONE
;
162 pbar_size
= PBAR_SIZE_DEFAULT
;
163 status_format_custom
= NULL
;
164 status_updates_per_second
= STATUS_UPDATES_PER_SEC
;
168 /* Process config options (overriding defaults) */
169 if (config
!= NULL
) {
170 if (config
->aprompt
!= NULL
) {
171 if (aprompt
== NULL
) {
172 aprompt
= malloc(DPV_APROMPT_MAX
);
176 snprintf(aprompt
, DPV_APROMPT_MAX
, "%s",
179 if (config
->pprompt
!= NULL
) {
180 if (pprompt
== NULL
) {
181 pprompt
= malloc(DPV_PPROMPT_MAX
+ 2);
182 /* +2 is for implicit "\n" appended later */
186 snprintf(pprompt
, DPV_APROMPT_MAX
, "%s",
190 options
= config
->options
;
191 action
= config
->action
;
192 backtitle
= config
->backtitle
;
193 debug
= config
->debug
;
194 dialog_test
= ((options
& DPV_TEST_MODE
) != 0);
195 dialog_updates_per_second
= config
->dialog_updates_per_second
;
196 display_limit
= config
->display_limit
;
197 display_type
= config
->display_type
;
198 keep_tite
= config
->keep_tite
;
199 label_size
= config
->label_size
;
200 msg_done
= (char *)config
->msg_done
;
201 msg_fail
= (char *)config
->msg_fail
;
202 msg_pending
= (char *)config
->msg_pending
;
203 no_labels
= ((options
& DPV_NO_LABELS
) != 0);
204 no_overrun
= ((options
& DPV_NO_OVERRUN
) != 0);
205 output
= config
->output
;
206 output_type
= config
->output_type
;
207 pbar_size
= config
->pbar_size
;
208 status_updates_per_second
= config
->status_updates_per_second
;
209 title
= config
->title
;
210 wide
= ((options
& DPV_WIDE_MODE
) != 0);
212 /* Enforce some minimums (pedantic) */
213 if (display_limit
< -1)
220 /* For the mini-pbar, -1 means hide, zero is invalid unless
221 * only one file is given */
222 if (pbar_size
== 0) {
223 if (file_list
== NULL
|| file_list
->next
== NULL
)
226 pbar_size
= PBAR_SIZE_DEFAULT
;
229 /* For the label, -1 means auto-size, zero is invalid unless
230 * specifically requested through the use of options flag */
231 if (label_size
== 0 && no_labels
== FALSE
)
232 label_size
= LABEL_SIZE_DEFAULT
;
234 /* Status update should not be zero */
235 if (status_updates_per_second
== 0)
236 status_updates_per_second
= STATUS_UPDATES_PER_SEC
;
237 } /* config != NULL */
239 /* Process the type of display we've been requested to produce */
240 switch (display_type
) {
241 case DPV_DISPLAY_STDOUT
:
245 use_libdialog
= FALSE
;
248 case DPV_DISPLAY_DIALOG
:
251 use_libdialog
= FALSE
;
254 case DPV_DISPLAY_XDIALOG
:
255 snprintf(dialog
, PATH_MAX
, XDIALOG
);
258 use_libdialog
= FALSE
;
264 use_libdialog
= TRUE
;
269 /* Enforce additional minimums that require knowing our display type */
270 if (dialog_updates_per_second
== 0)
271 dialog_updates_per_second
= use_xdialog
?
272 XDIALOG_UPDATES_PER_SEC
: DIALOG_UPDATES_PER_SEC
;
274 /* Allow forceful override of use_color */
275 if (config
!= NULL
&& (config
->options
& DPV_USE_COLOR
) != 0)
278 /* Count the number of files in provided list of dpv_file_node's */
279 if (use_dialog
&& pprompt
!= NULL
&& *pprompt
!= '\0')
280 pprompt_nls
= dialog_prompt_nlstate(pprompt
);
282 max_cols
= dialog_maxcols();
283 if (label_size
== -1)
284 shrink_label_size
= TRUE
;
286 /* Process file arguments */
287 for (curfile
= file_list
; curfile
!= NULL
; curfile
= curfile
->next
) {
290 /* dialog(3) only expands literal newlines */
291 if (use_libdialog
) strexpandnl(curfile
->name
);
293 /* Optionally calculate label size for file */
294 if (shrink_label_size
) {
296 name
= curfile
->name
;
297 if (curfile
== file_list
)
299 last
= (char *)dialog_prompt_lastline(name
, nls
);
303 nls
= dialog_prompt_nlstate(name
);
306 len
= dialog_prompt_longestline(last
, nls
);
307 if ((int)len
> (label_size
- 3)) {
311 /* Room for ellipsis (unless NULL) */
316 if (max_cols
> 0 && label_size
> (max_cols
- pbar_size
318 label_size
= max_cols
- pbar_size
- 9;
322 warnx("label=[%s] path=[%s] size=%lli",
323 curfile
->name
, curfile
->path
, curfile
->length
);
326 /* Optionally process the contents of DIALOGRC (~/.dialogrc) */
328 res
= parse_dialogrc();
329 if (debug
&& res
== 0) {
330 warnx("Successfully read `%s' config file", DIALOGRC
);
331 warnx("use_shadow = %i (Boolean)", use_shadow
);
332 warnx("use_colors = %i (Boolean)", use_colors
);
333 warnx("gauge_color=[%s] (FBH)", gauge_color
);
335 } else if (use_libdialog
) {
336 init_dialog(stdin
, stdout
);
337 use_shadow
= dialog_state
.use_shadow
;
338 use_colors
= dialog_state
.use_colors
;
339 gauge_color
[0] = 48 + dlg_color_table
[GAUGE_ATTR
].fg
;
340 gauge_color
[1] = 48 + dlg_color_table
[GAUGE_ATTR
].bg
;
341 gauge_color
[2] = dlg_color_table
[GAUGE_ATTR
].hilite
?
343 gauge_color
[3] = '\0';
346 warnx("Finished initializing dialog(3) library");
347 warnx("use_shadow = %i (Boolean)", use_shadow
);
348 warnx("use_colors = %i (Boolean)", use_colors
);
349 warnx("gauge_color=[%s] (FBH)", gauge_color
);
353 /* Enable mini progress bar automatically for stdin streams if unable
354 * to calculate progress (missing `lines:' syntax). */
355 if (dpv_nfiles
<= 1 && file_list
!= NULL
&& file_list
->length
< 0 &&
357 pbar_size
= PBAR_SIZE_DEFAULT
;
359 /* If $USE_COLOR is set and non-NULL enable color; otherwise disable */
360 if ((cp
= getenv(ENV_USE_COLOR
)) != 0)
361 use_color
= *cp
!= '\0' ? 1 : 0;
363 /* Print error and return `-1' if not given at least one name */
364 if (dpv_nfiles
== 0) {
365 warnx("%s: no labels provided", __func__
);
368 warnx("%s: %u label%s provided", __func__
, dpv_nfiles
,
369 dpv_nfiles
== 1 ? "" : "s");
371 /* If only one file and pbar size is zero, default to `-1' */
372 if (dpv_nfiles
<= 1 && pbar_size
== 0)
375 /* Print some debugging information */
377 warnx("%s: %s(%i) max rows x cols = %i x %i",
378 __func__
, use_xdialog
? XDIALOG
: DIALOG
,
379 use_libdialog
? 3 : 1, dialog_maxrows(),
383 /* Xdialog(1) updates a lot slower than dialog(1) */
384 if (dialog_test
&& use_xdialog
)
385 increment
= XDIALOG_INCREMENT
;
387 /* Always add implicit newline to pprompt (when specified) */
388 if (pprompt
!= NULL
&& *pprompt
!= '\0') {
389 len
= strlen(pprompt
);
391 * NOTE: pprompt = malloc(PPROMPT_MAX + 2)
392 * NOTE: (see getopt(2) section above for pprompt allocation)
394 pprompt
[len
++] = '\\';
395 pprompt
[len
++] = 'n';
396 pprompt
[len
++] = '\0';
399 /* Xdialog(1) requires newlines (a) escaped and (b) in triplicate */
400 if (use_xdialog
&& pprompt
!= NULL
) {
401 /* Replace `\n' with `\n\\n\n' in pprompt */
402 len
= strlen(pprompt
);
403 len
+= strcount(pprompt
, "\\n") * 2;
404 if (len
> DPV_PPROMPT_MAX
)
405 errx(EXIT_FAILURE
, "%s: Oops, pprompt buffer overflow "
406 "(%zu > %i)", __func__
, len
, DPV_PPROMPT_MAX
);
407 if (replaceall(pprompt
, "\\n", "\n\\n\n") < 0)
408 err(EXIT_FAILURE
, "%s: replaceall()", __func__
);
410 /* libdialog requires literal newlines */
411 else if (use_libdialog
&& pprompt
!= NULL
)
412 strexpandnl(pprompt
);
414 /* Xdialog(1) requires newlines (a) escaped and (b) in triplicate */
415 if (use_xdialog
&& aprompt
!= NULL
) {
416 /* Replace `\n' with `\n\\n\n' in aprompt */
417 len
= strlen(aprompt
);
418 len
+= strcount(aprompt
, "\\n") * 2;
419 if (len
> DPV_APROMPT_MAX
)
420 errx(EXIT_FAILURE
, "%s: Oops, aprompt buffer overflow "
421 " (%zu > %i)", __func__
, len
, DPV_APROMPT_MAX
);
422 if (replaceall(aprompt
, "\\n", "\n\\n\n") < 0)
423 err(EXIT_FAILURE
, "%s: replaceall()", __func__
);
425 /* libdialog requires literal newlines */
426 else if (use_libdialog
&& aprompt
!= NULL
)
427 strexpandnl(aprompt
);
430 * Warn user about an obscure dialog(1) bug (neither Xdialog(1) nor
431 * libdialog are affected) in the `--gauge' widget. If the first non-
432 * whitespace letter of "{new_prompt}" in "XXX\n{new_prompt}\nXXX\n"
433 * is a number, the number can sometimes be mistaken for a percentage
434 * to the overall progressbar. Other nasty side-effects such as the
435 * entire prompt not displaying or displaying improperly are caused by
438 * NOTE: When we can use color, we have a work-around... prefix the
439 * output with `\Zn' (used to terminate ANSI and reset to normal).
441 if (use_dialog
&& !use_color
) {
444 /* First, check pprompt (falls through if NULL) */
446 while (fc
!= NULL
&& *fc
!= '\0') {
447 if (*fc
== '\n') /* leading literal newline OK */
449 if (!isspace(*fc
) && *fc
!= '\\' && backslash
== 0)
451 else if (backslash
> 0 && *fc
!= 'n')
453 else if (*fc
== '\\') {
456 break; /* we're safe */
460 /* First non-whitespace character that dialog(1) will see */
461 if (fc
!= NULL
&& *fc
>= '0' && *fc
<= '9')
462 warnx("%s: WARNING! text argument to `-p' begins with "
463 "a number (not recommended)", __func__
);
464 else if (fc
> pprompt
)
465 warnx("%s: WARNING! text argument to `-p' begins with "
466 "whitespace (not recommended)", __func__
);
469 * If no pprompt or pprompt is all whitespace, check the first
470 * file name provided to make sure it is alright too.
472 if ((pprompt
== NULL
|| *fc
== '\0') && file_list
!= NULL
) {
473 first_file
= file_list
;
474 fc
= first_file
->name
;
475 while (fc
!= NULL
&& *fc
!= '\0' && isspace(*fc
))
477 /* First non-whitespace char that dialog(1) will see */
478 if (fc
!= NULL
&& *fc
>= '0' && *fc
<= '9')
479 warnx("%s: WARNING! File name `%s' begins "
480 "with a number (use `-p text' for safety)",
481 __func__
, first_file
->name
);
485 dprompt_init(file_list
);
486 /* Reads: label_size pbar_size pprompt aprompt dpv_nfiles */
487 /* Inits: dheight and dwidth */
489 /* Default localeconv(3) settings for dialog(3) status */
490 setlocale(LC_NUMERIC
,
491 getenv("LC_ALL") == NULL
&& getenv("LC_NUMERIC") == NULL
?
492 LC_NUMERIC_DEFAULT
: "");
495 /* Internally create the initial `--gauge' prompt text */
496 dprompt_recreate(file_list
, (struct dpv_file_node
*)NULL
, 0);
498 /* Spawn [X]dialog(1) `--gauge', returning pipe descriptor */
501 dprompt_libprint(pprompt
, aprompt
, 0);
503 dprompt_sprint(init_prompt
, pprompt
, aprompt
);
504 dialog_out
= dialog_spawn_gauge(init_prompt
, &pid
);
505 dprompt_dprint(dialog_out
, pprompt
, aprompt
, 0);
509 /* Seed the random(3) generator */
513 /* Set default/custom status line format */
514 if (dpv_nfiles
> 1) {
515 snprintf(status_format_default
, DPV_STATUS_FORMAT_MAX
, "%s",
517 status_format_custom
= config
->status_many
;
519 snprintf(status_format_default
, DPV_STATUS_FORMAT_MAX
, "%s",
521 status_format_custom
= config
->status_solo
;
524 /* Add test mode identifier to default status line if enabled */
525 if (dialog_test
&& (strlen(status_format_default
) + 12) <
526 DPV_STATUS_FORMAT_MAX
)
527 strcat(status_format_default
, " [TEST MODE]");
529 /* Verify custom status format */
530 status_fmt
= fmtcheck(status_format_custom
, status_format_default
);
531 if (status_format_custom
!= NULL
&&
532 status_fmt
== status_format_default
) {
533 warnx("WARNING! Invalid status_format configuration `%s'",
534 status_format_custom
);
535 warnx("Default status_format `%s'", status_format_default
);
538 /* Record when we started (used to prevent updating too quickly) */
539 (void)gettimeofday(&start
, (struct timezone
*)NULL
);
541 /* Calculate number of microseconds in-between sub-second updates */
542 if (status_updates_per_second
!= 0)
543 status_update_usec
= 1000000 / status_updates_per_second
;
544 if (dialog_updates_per_second
!= 0)
545 dialog_update_usec
= 1000000 / dialog_updates_per_second
;
548 * Process the file list [serially] (one for each argument passed)
550 files_left
= dpv_nfiles
;
551 list_head
= file_list
;
552 for (curfile
= file_list
; curfile
!= NULL
; curfile
= curfile
->next
) {
564 /* Attempt to spawn output program for this file */
565 if (!dialog_test
&& output
!= NULL
) {
569 switch (output_type
) {
570 case DPV_OUTPUT_SHELL
:
571 output_out
= shell_spawn_pipecmd(output
,
572 curfile
->name
, &output_pid
);
574 case DPV_OUTPUT_FILE
:
575 path_fmt
= fmtcheck(output
, "%s");
576 if (path_fmt
== output
)
577 len
= snprintf(pathbuf
,
578 PATH_MAX
, output
, curfile
->name
);
580 len
= snprintf(pathbuf
,
581 PATH_MAX
, "%s", output
);
582 if (len
>= PATH_MAX
) {
583 warnx("%s:%d:%s: pathbuf[%u] too small"
584 "to hold output argument",
585 __FILE__
, __LINE__
, __func__
,
589 if ((output_out
= open(pathbuf
,
590 O_CREAT
|O_WRONLY
, DEFFILEMODE
& ~mask
))
601 while (!dpv_interrupt
&& keep_going
) {
606 (int)(random() / 512 / dpv_nfiles
);
607 /* 512 limits fake readout to Megabytes */
608 } else if (action
!= NULL
)
609 pct
= action(curfile
, output_out
);
611 if (no_overrun
|| dialog_test
)
612 keep_going
= (pct
< 100);
614 status
= curfile
->status
;
615 keep_going
= (status
== DPV_STATUS_RUNNING
);
618 /* Get current time and calculate seconds elapsed */
619 gettimeofday(&now
, (struct timezone
*)NULL
);
620 now
.tv_sec
= now
.tv_sec
- start
.tv_sec
;
621 now
.tv_usec
= now
.tv_usec
- start
.tv_usec
;
623 now
.tv_sec
--, now
.tv_usec
+= 1000000;
624 seconds
= now
.tv_sec
+ (now
.tv_usec
/ 1000000.0);
626 /* Update dialog (be it dialog(3), dialog(1), etc.) */
627 if ((dialog_updates_per_second
!= 0 &&
629 seconds
!= dialog_old_seconds
||
630 now
.tv_usec
- dialog_last_update
>=
631 dialog_update_usec
||
632 nthfile
!= dialog_old_nthfile
635 /* Calculate overall progress (rounding up) */
636 overall
= (100 * nthfile
- 100 + pct
) /
638 if (((100 * nthfile
- 100 + pct
) * 10 /
639 dpv_nfiles
% 100) > 50)
642 dprompt_recreate(list_head
, curfile
, pct
);
644 if (use_libdialog
&& !debug
) {
645 /* Update dialog(3) widget */
646 dprompt_libprint(pprompt
, aprompt
,
649 /* stdout, dialog(1), or Xdialog(1) */
650 dprompt_dprint(dialog_out
, pprompt
,
654 dialog_old_seconds
= seconds
;
655 dialog_old_nthfile
= nthfile
;
656 dialog_last_update
= now
.tv_usec
;
659 /* Update the status line */
660 if ((use_libdialog
&& !debug
) &&
661 status_updates_per_second
!= 0 &&
663 keep_going
!= TRUE
||
664 seconds
!= status_old_seconds
||
665 now
.tv_usec
- status_last_update
>=
666 status_update_usec
||
667 nthfile
!= status_old_nthfile
670 status_printf(status_fmt
, dpv_overall_read
,
671 (dpv_overall_read
/ (seconds
== 0 ? 1 :
673 1, /* XXX until we add parallelism XXX */
675 status_old_seconds
= seconds
;
676 status_old_nthfile
= nthfile
;
677 status_last_update
= now
.tv_usec
;
681 if (!dialog_test
&& output_out
>= 0) {
683 waitpid(output_pid
, (int *)NULL
, 0);
689 /* Advance head of list when we hit the max display lines */
690 if (display_limit
> 0 && nthfile
% display_limit
== 0)
691 list_head
= curfile
->next
;
699 waitpid(pid
, (int *)NULL
, 0);
701 if (!keep_tite
&& !dpv_interrupt
)
704 warnx("%s: %lli overall read", __func__
, dpv_overall_read
);
706 if (dpv_interrupt
|| dpv_abort
)
713 * Free allocated items initialized by dpv()
720 dialog_maxsize_free();
721 if (aprompt
!= NULL
) {
725 if (pprompt
!= NULL
) {