Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F162309903
D58133.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
100 KB
Referenced Files
None
Subscribers
None
D58133.diff
View Options
diff --git a/etc/mtree/BSD.usr.dist b/etc/mtree/BSD.usr.dist
--- a/etc/mtree/BSD.usr.dist
+++ b/etc/mtree/BSD.usr.dist
@@ -10,6 +10,8 @@
private tags=package=clibs-dev
bsddialog tags=package=utilities-dev
..
+ bsdpv tags=package=utilities-dev
+ ..
bsdstat tags=package=libbsdstat-dev
..
event1 tags=package=libevent1-dev
diff --git a/lib/Makefile b/lib/Makefile
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -37,6 +37,7 @@
libbegemot \
libblocksruntime \
libbsddialog \
+ libbsdpv \
libbsdstat \
libbsm \
libbz2 \
@@ -123,6 +124,7 @@
SUBDIR_DEPEND_libarchive= libz libbz2 libexpat liblzma libmd libzstd
SUBDIR_DEPEND_libauditdm= libbsm
SUBDIR_DEPEND_libbsddialog= ncurses
+SUBDIR_DEPEND_libbsdpv= libbsddialog ncurses libutil
SUBDIR_DEPEND_libbsnmp= ${_libnetgraph}
SUBDIR_DEPEND_libc++:= libcxxrt
# libssp_nonshared doesn't need to be linked into libc on every arch, but it is
diff --git a/lib/libbsdpv/Makefile b/lib/libbsdpv/Makefile
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/Makefile
@@ -0,0 +1,16 @@
+PACKAGE= bsdpv
+LIB= bsdpv
+PRIVATELIB= yes
+SHLIB_MAJOR= 0
+INCS= bsdpv.h
+MAN= bsdpv.3
+MLINKS= bsdpv.3 bsdpv_end.3 \
+ bsdpv.3 bsdpv_free.3
+
+LIBADD= bsddialog util tinfow ncursesw
+
+SRCS= bsdpv.c display.c dprompt.c status.c util.c
+
+CFLAGS+= -I${.CURDIR} -I${SRCTOP}/contrib/bsddialog/lib
+
+.include <bsd.lib.mk>
diff --git a/lib/libbsdpv/Makefile.depend b/lib/libbsdpv/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/Makefile.depend
@@ -0,0 +1,20 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libbsddialog \
+ lib/libc \
+ lib/libcompiler_rt \
+ lib/libutil \
+ lib/msun \
+ lib/ncurses/ncurses \
+ lib/ncurses/tinfo \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/lib/libbsdpv/bsdpv.h b/lib/libbsdpv/bsdpv.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/bsdpv.h
@@ -0,0 +1,143 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _BSDPV_H_
+#define _BSDPV_H_
+
+#include <sys/types.h>
+
+#ifndef TRUE
+#define TRUE 1
+#endif
+#ifndef FALSE
+#define FALSE 0
+#endif
+
+/* localeconv(3) */
+#define LC_NUMERIC_DEFAULT "en_US.ISO8859-1"
+
+/* Data to process */
+extern long long bsdpv_overall_read;
+
+/* Interrupt flag */
+extern int bsdpv_interrupt; /* Set to TRUE in interrupt handler */
+extern int bsdpv_abort; /* Set to true in callback to abort */
+
+/*
+ * Display types for use with display_type member of bsdpv_config structure
+ */
+enum bsdpv_display {
+ BSDPV_DISPLAY_BSDDIALOG = 0, /* Display with bsddialog(3) */
+ BSDPV_DISPLAY_STDOUT, /* Display on stdout */
+};
+
+/*
+ * Output types for use with output_type member of bsdpv_config structure
+ */
+enum bsdpv_output {
+ BSDPV_OUTPUT_NONE = 0, /* No output (default) */
+ BSDPV_OUTPUT_FILE, /* Read `output' member as file path */
+ BSDPV_OUTPUT_SHELL, /* Read `output' member as shell cmd */
+};
+
+/*
+ * Activity types for use with status member of bsdpv_file_node structure.
+ * If you set a status other than BSDPV_STATUS_RUNNING on the current file in
+ * the action callback of bsdpv_config structure, you'll end callbacks for
+ * that bsdpv_file_node.
+ */
+enum bsdpv_status {
+ BSDPV_STATUS_RUNNING = 0, /* Running (default) */
+ BSDPV_STATUS_DONE, /* Completed */
+ BSDPV_STATUS_FAILED, /* Oops, something went wrong */
+};
+
+/*
+ * Anatomy of file option; pass an array of these as bsdpv() file_list
+ * argument terminated with a NULL pointer.
+ */
+struct bsdpv_file_node {
+ enum bsdpv_status status; /* status of read operation */
+ char *msg; /* display instead of "Done/Fail" */
+ char *name; /* name of file to read */
+ char *path; /* path to file */
+ long long length; /* expected size */
+ long long read; /* number units read (e.g., bytes) */
+ struct bsdpv_file_node *next; /* pointer to next (end with NULL) */
+};
+
+/*
+ * Anatomy of config option to pass as bsdpv() config argument
+ */
+struct bsdpv_config {
+ uint8_t keep_tite; /* Prevent visually distracting exit */
+ enum bsdpv_display display_type; /* Def. BSDPV_DISPLAY_BSDDIALOG */
+ enum bsdpv_output output_type; /* Output (default TYPE_NONE) */
+ int debug; /* Enable debugging output on stderr */
+ int display_limit; /* Files per `page'. Default -1 */
+ int label_size; /* Label size. Default 28 */
+ int pbar_size; /* Mini-progress size. See bsdpv(3) */
+ int display_updates_per_second; /* Progress updates/s. Def. 16 */
+ int status_updates_per_second; /* Status line updates/second.
+ * Default 2 */
+ uint16_t options; /* Special options. Default 0 */
+ char *title; /* widget title */
+ char *backtitle; /* Widget backtitle */
+ char *aprompt; /* Prompt append. Default NULL */
+ char *pprompt; /* Prompt prefix. Default NULL */
+ char *msg_done; /* Progress text. Default `Done' */
+ char *msg_fail; /* Progress text. Default `Fail' */
+ char *msg_pending; /* Progress text. Default `Pending' */
+ char *output; /* Output format string; see bsdpv(3) */
+ const char *status_solo; /* solo-status format.
+ * Default BSDPV_STATUS_SOLO */
+ const char *status_many; /* many-status format.
+ * Default BSDPV_STATUS_MANY */
+
+ /*
+ * Function pointer; action to perform data transfer
+ */
+ int (*action)(struct bsdpv_file_node *file, int out);
+};
+
+/*
+ * Macros for bsdpv() options bitmask argument
+ */
+#define BSDPV_TEST_MODE 0x0001 /* Test mode (fake reading data) */
+#define BSDPV_WIDE_MODE 0x0002 /* prefix/append bump dialog width */
+#define BSDPV_NO_LABELS 0x0004 /* Hide file_node.name labels */
+#define BSDPV_USE_COLOR 0x0008 /* Override to force color output */
+#define BSDPV_NO_OVERRUN 0x0010 /* Stop transfers when they hit 100% */
+
+/*
+ * Limits (modify with extreme care)
+ */
+#define BSDPV_APROMPT_MAX 4096 /* Buffer size for `-a text' */
+#define BSDPV_DISPLAY_LIMIT 10 /* Max file progress lines */
+#define BSDPV_PPROMPT_MAX 4096 /* Buffer size for `-p text' */
+#define BSDPV_STATUS_FORMAT_MAX 80 /* Buffer size for `-u format' */
+
+/*
+ * Extra display information
+ */
+#define BSDPV_STATUS_SOLO "%'10lli bytes read @ %'9.1f bytes/sec."
+#define BSDPV_STATUS_MANY (BSDPV_STATUS_SOLO " [%i/%i busy/wait]")
+
+/*
+ * Strings
+ */
+#define BSDPV_DONE_DEFAULT "Done"
+#define BSDPV_FAIL_DEFAULT "Fail"
+#define BSDPV_PENDING_DEFAULT "Pending"
+
+__BEGIN_DECLS
+void bsdpv_end(void);
+void bsdpv_free(void);
+int bsdpv(struct bsdpv_config *_config,
+ struct bsdpv_file_node *_file_list);
+__END_DECLS
+
+#endif /* !_BSDPV_H_ */
diff --git a/lib/libbsdpv/bsdpv.3 b/lib/libbsdpv/bsdpv.3
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/bsdpv.3
@@ -0,0 +1,433 @@
+.\"-
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2013-2026 Devin Teske
+.\"
+.Dd July 9, 2026
+.Dt BSDPV 3
+.Os
+.Sh NAME
+.Nm bsdpv
+.Nd BSD progress view library
+.Sh LIBRARY
+.Lb libbsdpv
+.Sh SYNOPSIS
+.In bsdpv.h
+.Ft int
+.Fo bsdpv
+.Fa "struct bsdpv_config *config"
+.Fa "struct bsdpv_file_node *file_list"
+.Fc
+.Ft void
+.Fo bsdpv_end
+.Fa "void"
+.Fc
+.Ft void
+.Fo bsdpv_free
+.Fa "void"
+.Fc
+.Sh DESCRIPTION
+The
+.Nm
+library provides an interface for creating complex
+.Dq gauge
+widgets for displaying progress on various actions.
+The
+.Nm
+library displays progress with
+.Xr bsddialog 3
+or on standard output.
+.Pp
+The
+.Nm
+library is a direct descendant of the
+.Ql libdpv
+.Dq dialog progress view
+library,
+rewritten to use
+.Xr bsddialog 3
+in place of the LGPL-licensed
+.Ql dialog
+library it once required.
+.Pp
+The
+.Fn bsdpv
+.Fa config
+argument properties for configuring global display features:
+.Bd -literal -offset indent
+struct bsdpv_config {
+ uint8_t keep_tite; /* Cleaner exit for scripts */
+ enum bsdpv_display display_type; /* Def. BSDPV_DISPLAY_BSDDIALOG */
+ enum bsdpv_output output_type; /* Default BSDPV_OUTPUT_NONE */
+ int debug; /* Enable debug on stderr */
+ int display_limit; /* Files/page. Default -1 */
+ int label_size; /* Label size. Default 28 */
+ int pbar_size; /* Mini-progress size */
+ int display_updates_per_second; /* Default 16 */
+ int status_updates_per_second; /* Default 2 */
+ uint16_t options; /* Default 0 (none) */
+ char *title; /* Widget title */
+ char *backtitle; /* Widget backtitle */
+ char *aprompt; /* Append. Default NULL */
+ char *pprompt; /* Prefix. Default NULL */
+ char *msg_done; /* Default `Done' */
+ char *msg_fail; /* Default `Fail' */
+ char *msg_pending; /* Default `Pending' */
+ char *output; /* Output format string */
+ const char *status_solo; /* solo-status format.
+ * Default BSDPV_STATUS_SOLO */
+ const char *status_many; /* many-status format.
+ * Default BSDPV_STATUS_MANY */
+
+ /*
+ * Function pointer; action to perform data transfer
+ */
+ int (*action)(struct bsdpv_file_node *file, int out);
+};
+
+enum bsdpv_display {
+ BSDPV_DISPLAY_BSDDIALOG = 0, /* Use bsddialog(3) (default) */
+ BSDPV_DISPLAY_STDOUT, /* Use stdout */
+};
+
+enum bsdpv_output {
+ BSDPV_OUTPUT_NONE = 0, /* No output (default) */
+ BSDPV_OUTPUT_FILE, /* Read `output' member as file path */
+ BSDPV_OUTPUT_SHELL, /* Read `output' member as shell cmd */
+};
+.Ed
+.Pp
+The
+.Va options
+member of the
+.Fn bsdpv
+.Fa config
+argument is a mask of bit fields indicating various processing options.
+Possible flags are:
+.Bl -tag -width BSDPV_NO_OVERRUN
+.It Dv BSDPV_TEST_MODE
+Enable test mode.
+In test mode,
+the
+.Fn action
+callback of the
+.Fa config
+argument is not called but instead simulated-data is used to drive progress.
+Appends
+.Dq [TEST MODE]
+to the status line
+.Po
+to override,
+set the
+.Va status_solo
+or
+.Va status_many
+members of the
+.Fn bsdpv
+.Fa config
+argument
+.Pc .
+.It Dv BSDPV_WIDE_MODE
+Enable wide mode.
+In wide mode,
+the length of the
+.Va aprompt
+and
+.Va pprompt
+members of the
+.Fn bsdpv
+.Fa config
+argument will bump the width of the gauge widget.
+Prompts wider than the maximum width will wrap.
+.It Dv BSDPV_NO_LABELS
+Disables the display of labels associated with each transfer
+.Po
+.Va label_size
+member of
+.Fn bsdpv
+.Fa config
+argument is ignored
+.Pc .
+.It Dv BSDPV_USE_COLOR
+Force the use of color
+.Po
+.Ev USE_COLOR
+environment variable is ignored
+.Pc .
+.It Dv BSDPV_NO_OVERRUN
+When enabled,
+callbacks for the current
+.Vt bsdpv_file_node
+are terminated when
+.Fn action
+returns 100 or greater
+.Po
+alleviates the need to change the
+.Va status
+of the current
+.Vt bsdpv_file_node
+but may also cause file truncation if the stream exceeds expected length
+.Pc .
+.El
+.Pp
+The
+.Fa file_list
+argument to
+.Fn bsdpv
+is a pointer to a
+.Dq linked-list ,
+described in
+.In bsdpv.h :
+.Bd -literal -offset indent
+struct bsdpv_file_node {
+ enum bsdpv_status status; /* status of read operation */
+ char *msg; /* display instead of "Done/Fail" */
+ char *name; /* name of file to read */
+ char *path; /* path to file */
+ long long length; /* expected size */
+ long long read; /* number units read (e.g., bytes) */
+ struct bsdpv_file_node *next;/* pointer to next (end with NULL) */
+};
+.Ed
+.Pp
+For each of the items in the
+.Fa file_list
+.Dq linked-list
+argument,
+the
+.Fn action
+callback member of the
+.Fn bsdpv
+.Fa config
+argument is called.
+The
+.Fn action
+function performs a
+.Dq nominal
+action on the file and return.
+The return value of
+.Vt int
+represents the current progress percentage
+.Pq 0-100
+for the current file.
+.Pp
+The
+.Fn action
+callback provides two variables for each call.
+.Fa file
+provides a reference to the current
+.Vt bsdpv_file_node
+being processed.
+.Fa out
+provides a file descriptor where the data goes.
+.Pp
+If the
+.Va output_type
+member of the
+.Fn bsdpv
+.Fa config
+argument was set to BSDPV_OUTPUT_NONE
+.Pq default ; when invoking Fn bsdpv ,
+the
+.Fa out
+file descriptor of
+.Fn action
+will be negative and can be ignored.
+If
+.Va output_type
+was set to BSDPV_OUTPUT_FILE,
+.Fa out
+will be an open file descriptor to a file.
+If
+.Va output_type
+was set to BSDPV_OUTPUT_SHELL,
+.Fa out
+will be an open file descriptor to a pipe for a spawned shell program.
+When
+.Fa out
+is greater than or equal to zero,
+write data that has been read back to
+.Fa out .
+.Pp
+To abort
+.Fn bsdpv ,
+either from the
+.Fn action
+callback or asynchronously from a signal handler,
+two globals are provided via
+.In bsdpv.h :
+.Bd -literal -offset indent
+extern int bsdpv_interrupt; /* Set to TRUE in interrupt handler */
+extern int bsdpv_abort; /* Set to true in callback to abort */
+.Ed
+.Pp
+These globals are not automatically reset and must be manually maintained.
+Do not forget to reset these globals before subsequent invocations of
+.Fn bsdpv
+when making multiple calls from the same program.
+.Pp
+In addition,
+the
+.Va status
+member of the
+.Fn action
+.Fa file
+argument can be used to control callbacks for the current file.
+The
+.Va status
+member can be set to any of the below from
+.In bsdpv.h :
+.Bd -literal -offset indent
+enum bsdpv_status {
+ BSDPV_STATUS_RUNNING = 0, /* Running (default) */
+ BSDPV_STATUS_DONE, /* Completed */
+ BSDPV_STATUS_FAILED, /* Oops, something went wrong */
+};
+.Ed
+.Pp
+The default
+.Fa status
+is zero,
+BSDPV_STATUS_RUNNING,
+which keeps the callbacks coming for the current
+.Fn file .
+Setting
+.Ql file->status
+to anything other than BSDPV_STATUS_RUNNING will cause
+.Fn bsdpv
+to loop to the next file,
+effecting the next callback,
+if any.
+.Pp
+The
+.Fn action
+callback is responsible for calculating percentages and
+.Pq recommended
+maintaining a
+.Nm
+global counter so
+.Fn bsdpv
+can display throughput statistics.
+Percentages are reported through the
+.Vt int
+return value of the
+.Fn action
+callback.
+Throughput statistics are calculated from the below global in
+.In bsdpv.h :
+.Bd -literal -offset indent
+extern long long bsdpv_overall_read;
+.Ed
+.Pp
+Set this to the number of bytes that have been read for all files.
+Throughput information is displayed in the status line at the bottom of the
+screen.
+.Pp
+Note that
+.Va bsdpv_overall_read
+does not have to represent bytes.
+For example,
+the
+.Va status_solo
+and
+.Va status_many
+formats can be changed to display something other than
+.Dq Li bytes
+and increment
+.Va bsdpv_overall_read
+accordingly
+.Pq for example, counting lines .
+.Pp
+When
+.Fn bsdpv
+is processing the current file,
+the
+.Va length
+and
+.Va read
+members of the
+.Fn action
+.Fa file
+argument are used for calculating the display of mini progress bars
+.Po
+if enabled;
+see
+.Va pbar_size
+above
+.Pc .
+If the
+.Va length
+member of the current
+.Fa file
+is less than zero
+.Pq indicating an unknown file length ,
+a
+.Xr humanize_number 3
+version of the
+.Va read
+member is used instead of a traditional progress bar.
+Otherwise a progress bar is calculated as percentage read to file length.
+.Fn action
+callback must maintain these member values for mini-progress bars.
+.Pp
+The
+.Fn bsdpv_end
+function ends
+.Xr bsddialog 3
+mode,
+restoring the terminal.
+It is safe to call
+.Fn bsdpv_end
+even when
+.Xr bsddialog 3
+mode is not active
+.Pq for example, from an error handler .
+.Pp
+The
+.Fn bsdpv_free
+function performs
+.Xr free 3
+on private global variables initialized by
+.Fn bsdpv .
+.Sh ENVIRONMENT
+The below environment variables are referenced by
+.Nm :
+.Bl -tag -width ".Ev USE_COLOR"
+.It Ev USE_COLOR
+If set and NULL,
+disables the use of color.
+.It Ev msg_done Ev msg_fail Ev msg_pending
+Internationalization strings for overriding the default English strings
+.Ql Done ,
+.Ql Fail ,
+and
+.Ql Pending
+respectively.
+To prevent their usage,
+explicitly set the
+.Va msg_done ,
+.Va msg_fail ,
+and
+.Va msg_pending
+members of
+.Fn bsdpv
+.Fa config
+argument to default macros
+.Pq BSDPV_DONE_DEFAULT, BSDPV_FAIL_DEFAULT, and BSDPV_PENDING_DEFAULT
+or desired values.
+.El
+.Sh SEE ALSO
+.Xr bsddialog 1 ,
+.Xr bsdpv 1 ,
+.Xr bsddialog 3
+.Sh HISTORY
+The
+.Nm
+library first appeared in
+.Fx 16.0 ,
+derived from the
+.Ql libdpv
+library that first appeared in
+.Fx 10.2 .
+.Sh AUTHORS
+.An Devin Teske Aq dteske@FreeBSD.org
diff --git a/lib/libbsdpv/bsdpv.c b/lib/libbsdpv/bsdpv.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/bsdpv.c
@@ -0,0 +1,653 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <bsddialog.h>
+#include <bsddialog_theme.h>
+
+#include "bsdpv.h"
+#include "bsdpv_private.h"
+#include "display.h"
+#include "dprompt.h"
+#include "status.h"
+#include "util.h"
+
+/* Test Mechanics (Only used when bsdpv_config.options |= BSDPV_TEST_MODE) */
+#define INCREMENT 1 /* Increment % per-pass test-mode */
+static uint8_t increment = INCREMENT;
+
+/* Debugging */
+uint8_t debug = FALSE;
+
+/* Data to process */
+int bsdpv_interrupt = FALSE;
+int bsdpv_abort = FALSE;
+unsigned int bsdpv_nfiles = 0;
+
+/* Data processing */
+long long bsdpv_overall_read = 0;
+static char pathbuf[PATH_MAX];
+
+/* Extra display information */
+uint8_t keep_tite = FALSE; /* bsdpv_config.keep_tite */
+uint8_t no_labels = FALSE; /* bsdpv_config.options & BSDPV_NO_LABELS */
+uint8_t wide = FALSE; /* bsdpv_config.options & BSDPV_WIDE_MODE */
+char *aprompt = NULL; /* bsdpv_config.aprompt */
+char *msg_done = NULL; /* bsdpv_config.msg_done */
+char *msg_fail = NULL; /* bsdpv_config.msg_fail */
+char *msg_pending = NULL; /* bsdpv_config.msg_pending */
+char *pprompt = NULL; /* bsdpv_config.pprompt */
+
+/* Status-Line format */
+static const char *status_format_custom = NULL;
+static char status_format_default[BSDPV_STATUS_FORMAT_MAX];
+
+/*
+ * Takes a pointer to a bsdpv_config structure containing layout details and
+ * pointer to initial element in a linked-list of bsdpv_file_node structures,
+ * each presenting a file to process. Executes the `action' function passed-in
+ * as a member to the `config' structure argument.
+ */
+int
+bsdpv(struct bsdpv_config *config, struct bsdpv_file_node *file_list)
+{
+ uint8_t keep_going;
+ uint8_t no_overrun = FALSE;
+ uint8_t shrink_label_size = FALSE;
+ mode_t mask;
+ uint16_t options;
+ char *cp;
+ char *last;
+ char *name;
+ char *output;
+ const char *status_fmt;
+ const char *path_fmt;
+ enum bsddialog_color bar_bg;
+ enum bsddialog_color bar_fg;
+ enum bsdpv_display display_type;
+ enum bsdpv_output output_type;
+ enum bsdpv_status status;
+ int (*action)(struct bsdpv_file_node *file, int out);
+ int display_did_update;
+ int display_last_update = 0;
+ int display_old_nthfile = 0;
+ int display_old_seconds = -1;
+ int display_update_usec = 0;
+ int display_updates_per_second;
+ int files_left;
+ int status_last_update = 0;
+ int status_old_nthfile = 0;
+ int status_old_seconds = -1;
+ int status_update_usec = 0;
+ int max_cols;
+ int nthfile = 0;
+ int output_out;
+ int overall = 0;
+ int pct;
+ int seconds;
+ int status_updates_per_second;
+ pid_t output_pid;
+ size_t len;
+ unsigned int bar_flags;
+ struct bsddialog_theme theme;
+ struct bsdpv_file_node *curfile;
+ struct bsdpv_file_node *lastfile;
+ struct bsdpv_file_node *list_head;
+ struct timeval now;
+ struct timeval start;
+
+ /* Initialize globals to default values */
+ aprompt = NULL;
+ pprompt = NULL;
+ options = 0;
+ action = NULL;
+ backtitle = NULL;
+ debug = FALSE;
+ display_test = FALSE;
+ display_updates_per_second = DISPLAY_UPDATES_PER_SEC;
+ display_limit = DISPLAY_LIMIT_DEFAULT;
+ display_type = BSDPV_DISPLAY_BSDDIALOG;
+ keep_tite = FALSE;
+ label_size = LABEL_SIZE_DEFAULT;
+ msg_done = NULL;
+ msg_fail = NULL;
+ msg_pending = NULL;
+ no_labels = FALSE;
+ output = NULL;
+ output_type = BSDPV_OUTPUT_NONE;
+ pbar_size = PBAR_SIZE_DEFAULT;
+ status_format_custom = NULL;
+ status_updates_per_second = STATUS_UPDATES_PER_SEC;
+ title = NULL;
+ wide = FALSE;
+
+ /* Process config options (overriding defaults) */
+ if (config != NULL) {
+ if (config->aprompt != NULL) {
+ if (aprompt == NULL) {
+ aprompt = malloc(BSDPV_APROMPT_MAX);
+ if (aprompt == NULL)
+ return (-1);
+ }
+ snprintf(aprompt, BSDPV_APROMPT_MAX, "%s",
+ config->aprompt);
+ }
+ if (config->pprompt != NULL) {
+ if (pprompt == NULL) {
+ pprompt = malloc(BSDPV_PPROMPT_MAX + 2);
+ /* +2 is for implicit "\n" appended later */
+ if (pprompt == NULL)
+ return (-1);
+ }
+ snprintf(pprompt, BSDPV_PPROMPT_MAX, "%s",
+ config->pprompt);
+ }
+
+ options = config->options;
+ action = config->action;
+ backtitle = config->backtitle;
+ debug = config->debug;
+ display_test = ((options & BSDPV_TEST_MODE) != 0);
+ display_updates_per_second =
+ config->display_updates_per_second;
+ display_limit = config->display_limit;
+ display_type = config->display_type;
+ keep_tite = config->keep_tite;
+ label_size = config->label_size;
+ msg_done = (char *)config->msg_done;
+ msg_fail = (char *)config->msg_fail;
+ msg_pending = (char *)config->msg_pending;
+ no_labels = ((options & BSDPV_NO_LABELS) != 0);
+ no_overrun = ((options & BSDPV_NO_OVERRUN) != 0);
+ output = config->output;
+ output_type = config->output_type;
+ pbar_size = config->pbar_size;
+ status_updates_per_second = config->status_updates_per_second;
+ title = config->title;
+ wide = ((options & BSDPV_WIDE_MODE) != 0);
+
+ /* Enforce some minimums (pedantic) */
+ if (display_limit < -1)
+ display_limit = -1;
+ if (label_size < -1)
+ label_size = -1;
+ if (pbar_size < -1)
+ pbar_size = -1;
+
+ /* For the mini-pbar, -1 means hide, zero is invalid unless
+ * only one file is given */
+ if (pbar_size == 0) {
+ if (file_list == NULL || file_list->next == NULL)
+ pbar_size = -1;
+ else
+ pbar_size = PBAR_SIZE_DEFAULT;
+ }
+
+ /* For the label, -1 means auto-size, zero is invalid unless
+ * specifically requested through the use of options flag */
+ if (label_size == 0 && no_labels == FALSE)
+ label_size = LABEL_SIZE_DEFAULT;
+
+ /* Status update should not be zero */
+ if (status_updates_per_second == 0)
+ status_updates_per_second = STATUS_UPDATES_PER_SEC;
+ } /* config != NULL */
+
+ /* Process the type of display we've been requested to produce */
+ switch (display_type) {
+ case BSDPV_DISPLAY_STDOUT:
+ debug = TRUE;
+ use_color = FALSE;
+ use_bsddialog = FALSE;
+ break;
+ default:
+ use_color = TRUE;
+ use_bsddialog = TRUE;
+ break;
+ } /* display_type */
+
+ /* Enforce additional minimums that require knowing our display type */
+ if (display_updates_per_second == 0)
+ display_updates_per_second = DISPLAY_UPDATES_PER_SEC;
+
+ /* Allow forceful override of use_color */
+ if (config != NULL && (config->options & BSDPV_USE_COLOR) != 0)
+ use_color = TRUE;
+
+ /* If $USE_COLOR is set and non-NULL enable color; otherwise disable */
+ if ((cp = getenv(ENV_USE_COLOR)) != 0)
+ use_color = *cp != '\0' ? 1 : 0;
+
+ /*
+ * Initialize bsddialog(3) and read the active theme
+ */
+ bsddialog_initconf(&dconf);
+ dconf.title = title;
+ dconf.text.escape = use_color;
+ dconf.auto_downmargin = 1; /* reserve bottom row for status_printf(3) */
+ if (use_bsddialog && !debug) {
+ enum bsddialog_default_theme deftheme;
+
+ if (bsddialog_init_notheme() != BSDDIALOG_OK) {
+ warnx("%s: %s", __func__, bsddialog_geterror());
+ return (-1);
+ }
+ /*
+ * Leave the alternate screen before applying the theme, as
+ * libdialog does before dlg_clear(3). Otherwise refresh(3)
+ * during keep-tite flushes a blue backdrop into scrollback.
+ */
+ if (!keep_tite)
+ bsddialog_keep_tite();
+ deftheme = bsddialog_hascolors() ? BSDDIALOG_THEME_FLAT :
+ BSDDIALOG_THEME_BLACKWHITE;
+ if (bsddialog_set_default_theme_noflush(deftheme) != BSDDIALOG_OK) {
+ warnx("%s: %s", __func__, bsddialog_geterror());
+ bsddialog_end();
+ return (-1);
+ }
+ bsddialog_fill_backdrop();
+ use_shadow = dconf.shadow;
+ use_colors = bsddialog_hascolors();
+ bsddialog_get_theme(&theme);
+ bar_fg = BSDDIALOG_WHITE;
+ bar_bg = BSDDIALOG_BLUE;
+ bar_flags = BSDDIALOG_BOLD;
+ bsddialog_color_attrs(theme.bar.f_color, &bar_fg, &bar_bg,
+ &bar_flags);
+ gauge_color[0] = '0' + bar_bg;
+ /* NB: filled portion is drawn in reverse video */
+ gauge_color[1] = '0' + bar_fg;
+ gauge_color[2] = (bar_flags & BSDDIALOG_BOLD) != 0 ?
+ 'b' : 'B';
+ gauge_color[3] = '\0';
+ }
+ if (debug) {
+ warnx("Finished initializing bsddialog(3) library");
+ warnx("use_shadow = %i (Boolean)", use_shadow);
+ warnx("use_colors = %i (Boolean)", use_colors);
+ warnx("gauge_color=[%s] (FBH)", gauge_color);
+ }
+
+ max_cols = display_maxcols();
+ if (label_size == -1)
+ shrink_label_size = TRUE;
+
+ /* Process file arguments */
+ for (curfile = file_list; curfile != NULL; curfile = curfile->next) {
+ bsdpv_nfiles++;
+
+ /* bsddialog(3) only expands literal newlines */
+ if (use_bsddialog)
+ strexpandnl(curfile->name);
+
+ /* Optionally calculate label size for file */
+ if (shrink_label_size) {
+ name = curfile->name;
+ last = prompt_lastline(name);
+ len = prompt_longestline(last);
+ if ((int)len > (label_size - 3)) {
+ if (label_size > 0)
+ label_size += 3;
+ label_size = len;
+ /* Room for ellipsis (unless NULL) */
+ if (label_size > 0)
+ label_size += 3;
+ }
+
+ if (max_cols > 0 && label_size > (max_cols - pbar_size
+ - 9))
+ label_size = max_cols - pbar_size - 9;
+ }
+
+ if (debug)
+ warnx("label=[%s] path=[%s] size=%lli",
+ curfile->name, curfile->path, curfile->length);
+ } /* file_list */
+
+ /* Enable mini progress bar automatically for stdin streams if unable
+ * to calculate progress (missing `lines:' syntax). */
+ if (bsdpv_nfiles <= 1 && file_list != NULL && file_list->length < 0 &&
+ !display_test)
+ pbar_size = PBAR_SIZE_DEFAULT;
+
+ /* Print error and return `-1' if not given at least one name */
+ if (bsdpv_nfiles == 0) {
+ warnx("%s: no labels provided", __func__);
+ return (-1);
+ } else if (debug)
+ warnx("%s: %u label%s provided", __func__, bsdpv_nfiles,
+ bsdpv_nfiles == 1 ? "" : "s");
+
+ /* If only one file and pbar size is zero, default to `-1' */
+ if (bsdpv_nfiles <= 1 && pbar_size == 0)
+ pbar_size = -1;
+
+ /* Print some debugging information */
+ if (debug)
+ warnx("%s: bsddialog(3) max rows x cols = %i x %i",
+ __func__, display_maxrows(), display_maxcols());
+
+ /* Always add implicit newline to pprompt (when specified) */
+ if (pprompt != NULL && *pprompt != '\0') {
+ len = strlen(pprompt);
+ /*
+ * NOTE: pprompt = malloc(BSDPV_PPROMPT_MAX + 2)
+ * NOTE: (see config section above for pprompt allocation)
+ */
+ pprompt[len++] = '\\';
+ pprompt[len++] = 'n';
+ pprompt[len++] = '\0';
+ }
+
+ /* bsddialog(3) requires literal newlines */
+ if (use_bsddialog) {
+ if (pprompt != NULL)
+ strexpandnl(pprompt);
+ if (aprompt != NULL)
+ strexpandnl(aprompt);
+ }
+
+ dprompt_init(file_list);
+ /* Reads: label_size pbar_size pprompt aprompt bsdpv_nfiles */
+ /* Inits: dheight and dwidth */
+ if (use_bsddialog && !debug)
+ status_init();
+
+ /* Default localeconv(3) settings for status line */
+ setlocale(LC_NUMERIC,
+ getenv("LC_ALL") == NULL && getenv("LC_NUMERIC") == NULL ?
+ LC_NUMERIC_DEFAULT : "");
+
+ /* Set default/custom status line format */
+ if (bsdpv_nfiles > 1) {
+ snprintf(status_format_default, BSDPV_STATUS_FORMAT_MAX, "%s",
+ BSDPV_STATUS_MANY);
+ status_format_custom = config != NULL ?
+ config->status_many : NULL;
+ } else {
+ snprintf(status_format_default, BSDPV_STATUS_FORMAT_MAX, "%s",
+ BSDPV_STATUS_SOLO);
+ status_format_custom = config != NULL ?
+ config->status_solo : NULL;
+ }
+
+ /* Add test mode identifier to default status line if enabled */
+ if (display_test && (strlen(status_format_default) + 12) <
+ BSDPV_STATUS_FORMAT_MAX)
+ strcat(status_format_default, " [TEST MODE]");
+
+ /* Verify custom status format */
+ status_fmt = fmtcheck(status_format_custom, status_format_default);
+ if (status_format_custom != NULL &&
+ status_fmt == status_format_default) {
+ warnx("WARNING! Invalid status_format configuration `%s'",
+ status_format_custom);
+ warnx("Default status_format `%s'", status_format_default);
+ }
+
+ if (!debug) {
+ /* Build initial prompt text; first physical draw is in loop */
+ dprompt_recreate(file_list, (struct bsdpv_file_node *)NULL, 0);
+ } /* !debug */
+
+ /* Seed the random(3) generator */
+ if (display_test)
+ srandom(0xf1eeface);
+
+ /* Record when we started (used to prevent updating too quickly) */
+ (void)gettimeofday(&start, (struct timezone *)NULL);
+
+ /* Calculate number of microseconds in-between sub-second updates */
+ if (status_updates_per_second != 0)
+ status_update_usec = 1000000 / status_updates_per_second;
+ if (display_updates_per_second != 0)
+ display_update_usec = 1000000 / display_updates_per_second;
+
+ /*
+ * Process the file list [serially] (one for each argument passed)
+ */
+ files_left = bsdpv_nfiles;
+ list_head = file_list;
+ lastfile = NULL;
+ for (curfile = file_list; curfile != NULL; curfile = curfile->next) {
+ lastfile = curfile;
+ keep_going = TRUE;
+ output_out = -1;
+ pct = 0;
+ nthfile++;
+ files_left--;
+
+ if (bsdpv_interrupt)
+ break;
+ if (display_test)
+ pct = 0 - increment;
+
+ /* Attempt to spawn output program for this file */
+ if (!display_test && output != NULL) {
+ mask = umask(0022);
+ (void)umask(mask);
+
+ switch (output_type) {
+ case BSDPV_OUTPUT_SHELL:
+ output_out = shell_spawn_pipecmd(output,
+ curfile->name, &output_pid);
+ break;
+ case BSDPV_OUTPUT_FILE:
+ path_fmt = fmtcheck(output, "%s");
+ if (path_fmt == output)
+ len = snprintf(pathbuf,
+ PATH_MAX, output, curfile->name);
+ else
+ len = snprintf(pathbuf,
+ PATH_MAX, "%s", output);
+ if (len >= PATH_MAX) {
+ warnx("%s:%d:%s: pathbuf[%u] too small"
+ "to hold output argument",
+ __FILE__, __LINE__, __func__,
+ PATH_MAX);
+ return (-1);
+ }
+ if ((output_out = open(pathbuf,
+ O_CREAT|O_WRONLY, DEFFILEMODE & ~mask))
+ < 0) {
+ warn("%s", pathbuf);
+ return (-1);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ while (!bsdpv_interrupt && keep_going) {
+ if (display_test) {
+ usleep(50000);
+ pct += increment;
+ bsdpv_overall_read +=
+ (int)(random() / 512 / bsdpv_nfiles);
+ /* 512 limits fake readout to Megabytes */
+ } else if (action != NULL)
+ pct = action(curfile, output_out);
+
+ if (no_overrun || display_test)
+ keep_going = (pct < 100);
+ else {
+ status = curfile->status;
+ keep_going = (status == BSDPV_STATUS_RUNNING);
+ }
+
+ /* Get current time and calculate seconds elapsed */
+ gettimeofday(&now, (struct timezone *)NULL);
+ now.tv_sec = now.tv_sec - start.tv_sec;
+ now.tv_usec = now.tv_usec - start.tv_usec;
+ if (now.tv_usec < 0)
+ now.tv_sec--, now.tv_usec += 1000000;
+ seconds = now.tv_sec + (now.tv_usec / 1000000.0);
+ display_did_update = FALSE;
+
+ /* Update the progress display */
+ if ((display_updates_per_second != 0 &&
+ (
+ seconds != display_old_seconds ||
+ now.tv_usec - display_last_update >=
+ display_update_usec ||
+ nthfile != display_old_nthfile
+ )) || pct >= 100 || keep_going != TRUE
+ ) {
+ /* Calculate overall progress (rounding up) */
+ overall = (100 * nthfile - 100 + pct) /
+ bsdpv_nfiles;
+ if (((100 * nthfile - 100 + pct) * 10 /
+ bsdpv_nfiles % 100) > 50)
+ overall++;
+
+ dprompt_recreate(list_head, curfile, pct);
+
+ if (!debug) {
+ /* Update bsddialog(3) widget */
+ dprompt_draw(pprompt, aprompt,
+ overall);
+ } else {
+ /* stdout */
+ dprompt_dprint(STDOUT_FILENO, pprompt,
+ aprompt, overall);
+ fsync(STDOUT_FILENO);
+ }
+ display_old_seconds = seconds;
+ display_old_nthfile = nthfile;
+ display_last_update = now.tv_usec;
+ display_did_update = TRUE;
+ }
+
+ /* Update the status line (independent of gauge rate) */
+ if (!debug && status_updates_per_second != 0 &&
+ (
+ keep_going != TRUE ||
+ pct >= 100 ||
+ seconds != status_old_seconds ||
+ now.tv_usec - status_last_update >=
+ status_update_usec ||
+ nthfile != status_old_nthfile
+ )
+ ) {
+ status_printf(status_fmt, bsdpv_overall_read,
+ (bsdpv_overall_read / (seconds == 0 ? 1 :
+ seconds) * 1.0),
+ 1, /* XXX until we add parallelism XXX */
+ files_left);
+ status_old_seconds = seconds;
+ status_old_nthfile = nthfile;
+ status_last_update = now.tv_usec;
+ } else if (display_did_update) {
+ status_backdrop_flush();
+ bsddialog_gauge_flush();
+ status_reflush();
+ }
+ }
+
+ if (!display_test && output_out >= 0) {
+ close(output_out);
+ waitpid(output_pid, (int *)NULL, 0);
+ }
+
+ if (bsdpv_abort)
+ break;
+
+ /* Advance head of list when we hit the max display lines */
+ if (display_limit > 0 && nthfile % display_limit == 0)
+ list_head = curfile->next;
+ }
+
+ if (!debug) {
+ if (bsddialog_inmode() && lastfile != NULL) {
+ if (!bsdpv_interrupt && !bsdpv_abort) {
+ dprompt_recreate(list_head, lastfile, 100);
+ dprompt_draw(pprompt, aprompt, 100);
+ bsddialog_gauge_flush();
+ if (status_updates_per_second != 0) {
+ gettimeofday(&now, (struct timezone *)NULL);
+ now.tv_sec = now.tv_sec - start.tv_sec;
+ now.tv_usec = now.tv_usec - start.tv_usec;
+ if (now.tv_usec < 0)
+ now.tv_sec--, now.tv_usec += 1000000;
+ seconds = now.tv_sec +
+ (now.tv_usec / 1000000.0);
+ status_printf(status_fmt, bsdpv_overall_read,
+ (bsdpv_overall_read / (seconds == 0 ? 1 :
+ seconds) * 1.0),
+ 1, files_left);
+ } else
+ status_reflush();
+ } else {
+ /*
+ * Match dpv(1): leave the last status line on
+ * screen after Ctrl-C or abort for examination.
+ */
+ status_reflush();
+ }
+ if (!keep_tite) {
+ if (bsdpv_interrupt || bsdpv_abort)
+ status_leave_no_nl();
+ else
+ status_leave();
+ }
+ }
+ bsdpv_end();
+ if (bsdpv_interrupt || bsdpv_abort)
+ status_restore_tty();
+ dprompt_gauge_close();
+ } else
+ warnx("%s: %lli overall read", __func__, bsdpv_overall_read);
+
+ if (bsdpv_interrupt || bsdpv_abort)
+ return (-1);
+ else
+ return (0);
+}
+
+/*
+ * End bsddialog(3) mode, restoring the terminal. Safe to call even when
+ * bsddialog(3) mode is not active (e.g., from an error path).
+ */
+void
+bsdpv_end(void)
+{
+ if (bsddialog_inmode())
+ bsddialog_end();
+}
+
+/*
+ * Free allocated items initialized by bsdpv()
+ */
+void
+bsdpv_free(void)
+{
+ dprompt_free();
+ display_maxsize_free();
+ if (aprompt != NULL) {
+ free(aprompt);
+ aprompt = NULL;
+ }
+ if (pprompt != NULL) {
+ free(pprompt);
+ pprompt = NULL;
+ }
+ status_free();
+}
diff --git a/lib/libbsdpv/bsdpv_private.h b/lib/libbsdpv/bsdpv_private.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/bsdpv_private.h
@@ -0,0 +1,43 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _BSDPV_PRIVATE_H_
+#define _BSDPV_PRIVATE_H_
+
+#include <sys/types.h>
+
+/* Debugging */
+extern uint8_t debug;
+
+/* Data to process */
+extern unsigned int bsdpv_nfiles;
+
+/* Extra display information */
+extern uint8_t keep_tite;
+extern uint8_t no_labels;
+extern uint8_t wide;
+extern char *msg_done, *msg_fail, *msg_pending;
+extern char *pprompt, *aprompt;
+
+/* Defaults */
+#define DISPLAY_UPDATES_PER_SEC 16
+#define DISPLAY_LIMIT_DEFAULT 0 /* Auto-calculate */
+#define LABEL_SIZE_DEFAULT 28
+#define PBAR_SIZE_DEFAULT 17
+#define STATUS_UPDATES_PER_SEC 2
+
+/* states for dprompt_add_files() of dprompt.c */
+enum dprompt_state {
+ DPROMPT_NONE = 0, /* Default */
+ DPROMPT_PENDING, /* Pending */
+ DPROMPT_PBAR, /* Progress bar */
+ DPROMPT_END_STATE, /* Done/Fail */
+ DPROMPT_DETAILS, /* bsdpv_file_node->read */
+ DPROMPT_CUSTOM_MSG, /* bsdpv_file_node->msg */
+ DPROMPT_MINIMAL, /* whitespace */
+};
+
+#endif /* !_BSDPV_PRIVATE_H_ */
diff --git a/lib/libbsdpv/display.h b/lib/libbsdpv/display.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/display.h
@@ -0,0 +1,41 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _DISPLAY_H_
+#define _DISPLAY_H_
+
+#include <sys/types.h>
+
+#include <bsddialog.h>
+
+/* bsddialog(3) characteristics */
+#define PROMPT_MAX 16384
+#define ENV_USE_COLOR "USE_COLOR"
+extern uint8_t display_test;
+extern uint8_t use_bsddialog;
+extern uint8_t use_color;
+extern uint8_t use_colors;
+extern uint8_t use_shadow;
+extern char gauge_color[];
+extern struct bsddialog_conf dconf;
+
+/* bsddialog(3) functionality */
+extern char *title, *backtitle;
+extern int dheight, dwidth;
+
+__BEGIN_DECLS
+void display_maxsize_free(void);
+char *prompt_lastline(char *_prompt);
+int display_maxcols(void);
+int display_maxrows(void);
+int prompt_wrappedlines(char *_prompt, int _ncols);
+int tty_maxcols(void);
+#define tty_maxrows() display_maxrows()
+unsigned int prompt_longestline(const char *_prompt);
+unsigned int prompt_numlines(const char *_prompt);
+__END_DECLS
+
+#endif /* !_DISPLAY_H_ */
diff --git a/lib/libbsdpv/display.c b/lib/libbsdpv/display.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/display.c
@@ -0,0 +1,291 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/ioctl.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "bsdpv.h"
+#include "bsdpv_private.h"
+#include "display.h"
+
+#define TTY_DEFAULT_ROWS 24
+#define TTY_DEFAULT_COLS 80
+
+/* bsddialog(3) characteristics */
+uint8_t display_test = 0;
+uint8_t use_bsddialog = 1;
+uint8_t use_color = 1;
+uint8_t use_colors = 1;
+uint8_t use_shadow = 1;
+char gauge_color[4] = "47b"; /* blue on white, bold */
+struct bsddialog_conf dconf;
+
+/* bsddialog(3) functionality */
+char *title = NULL;
+char *backtitle = NULL;
+int dheight = 0;
+int dwidth = 0;
+
+/* TTY/Screen characteristics */
+static struct winsize *maxsize = NULL;
+
+/*
+ * Update row/column fields of `maxsize' global (used by display_maxrows() and
+ * display_maxcols()). If the `maxsize' pointer is NULL, it will be
+ * initialized. The `ws_row' and `ws_col' fields of `maxsize' are updated to
+ * hold current maximum height and width (respectively) for a bsddialog(3)
+ * widget based on the active TTY size.
+ *
+ * This function is called automatically by display_maxrows/cols() to reflect
+ * changes in terminal size in-between calls.
+ */
+static void
+tty_maxsize_update(void)
+{
+ int fd = STDIN_FILENO;
+ struct termios t;
+
+ if (maxsize == NULL) {
+ if ((maxsize = malloc(sizeof(struct winsize))) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ memset((void *)maxsize, '\0', sizeof(struct winsize));
+ }
+
+ if (!isatty(fd))
+ fd = open("/dev/tty", O_RDONLY);
+ if ((tcgetattr(fd, &t) < 0) || (ioctl(fd, TIOCGWINSZ, maxsize) < 0)) {
+ maxsize->ws_row = TTY_DEFAULT_ROWS;
+ maxsize->ws_col = TTY_DEFAULT_COLS;
+ }
+}
+
+/*
+ * Return the current maximum height (rows) for a bsddialog(3) widget.
+ */
+int
+display_maxrows(void)
+{
+
+ tty_maxsize_update();
+ return (maxsize->ws_row);
+}
+
+/*
+ * Return the current maximum width (cols) for a bsddialog(3) widget.
+ */
+int
+display_maxcols(void)
+{
+
+ tty_maxsize_update();
+ if (use_bsddialog && use_shadow)
+ return (maxsize->ws_col - 2);
+ else
+ return (maxsize->ws_col);
+}
+
+/*
+ * Return the current maximum width (cols) for the terminal.
+ */
+int
+tty_maxcols(void)
+{
+
+ tty_maxsize_update();
+ return (maxsize->ws_col);
+}
+
+/*
+ * Returns the number of lines in buffer pointed to by `prompt'. Takes literal
+ * newlines into account (escaped newlines should be expanded beforehand; see
+ * strexpandnl() of util.c).
+ */
+unsigned int
+prompt_numlines(const char *prompt)
+{
+ const char *cp = prompt;
+ unsigned int nlines = 1;
+
+ if (prompt == NULL || *prompt == '\0')
+ return (0);
+
+ while (*cp != '\0') {
+ if (*cp == '\n')
+ nlines++;
+ cp++;
+ }
+
+ return (nlines);
+}
+
+/*
+ * Returns the length in bytes of the longest line in buffer pointed to by
+ * `prompt'. Takes newlines into account. Also discounts bsddialog(3) color
+ * escape codes if enabled (via `use_color' global).
+ */
+unsigned int
+prompt_longestline(const char *prompt)
+{
+ uint8_t backslash = 0;
+ const char *p = prompt;
+ int longest = 0;
+ int n = 0;
+
+ /* `prompt' parameter is required */
+ if (prompt == NULL)
+ return (0);
+ if (*prompt == '\0')
+ return (0); /* shortcut */
+
+ /* Loop until the end of the string */
+ while (*p != '\0') {
+ /* bsddialog(3) renders literal newlines */
+ if (*p == '\n') {
+ if (n > longest)
+ longest = n;
+ n = 0;
+ p++;
+ continue;
+ }
+
+ /* Check for backslash character */
+ if (*p == '\\') {
+ /* If second backslash, count as a single-char */
+ if ((backslash ^= 1) == 0)
+ n++;
+ } else if (backslash) {
+ if (use_color && *p == 'Z') {
+ if (*++p != '\0')
+ p++;
+ backslash = 0;
+ continue;
+ } else /* bsddialog(3) only expands \Z sequences */
+ n += 2;
+
+ backslash = 0;
+ } else
+ n++;
+ p++;
+ }
+ if (n > longest)
+ longest = n;
+
+ return (longest);
+}
+
+/*
+ * Returns a pointer to the last line in buffer pointed to by `prompt'. If no
+ * newlines appear in the buffer, `prompt' is returned. If passed a NULL
+ * pointer, returns NULL.
+ */
+char *
+prompt_lastline(char *prompt)
+{
+ char *lastline;
+ char *p;
+
+ if (prompt == NULL)
+ return (NULL);
+ if (*prompt == '\0')
+ return (prompt); /* shortcut */
+
+ lastline = p = prompt;
+ while (*p != '\0') {
+ if (*p == '\n')
+ lastline = p + 1;
+ p++;
+ }
+
+ return (lastline);
+}
+
+/*
+ * Returns the number of extra lines generated by wrapping the text in buffer
+ * pointed to by `prompt' within `ncols' columns (for prompts, this should be
+ * dwidth - 4). Also discounts bsddialog(3) color escape codes if enabled (via
+ * `use_color' global).
+ */
+int
+prompt_wrappedlines(char *prompt, int ncols)
+{
+ uint8_t backslash = 0;
+ char *cp;
+ char *p = prompt;
+ int n = 0;
+ int wlines = 0;
+
+ /* `prompt' parameter is required */
+ if (p == NULL)
+ return (0);
+ if (*p == '\0')
+ return (0); /* shortcut */
+
+ /* Loop until the end of the string */
+ while (*p != '\0') {
+ /* bsddialog(3) renders literal newlines */
+ if (*p == '\n')
+ n = 0;
+
+ /* Check for backslash character */
+ if (*p == '\\') {
+ /* If second backslash, count as a single-char */
+ if ((backslash ^= 1) == 0)
+ n++;
+ } else if (backslash) {
+ if (use_color && *p == 'Z') {
+ if (*++p != '\0')
+ p++;
+ backslash = 0;
+ continue;
+ } else /* bsddialog(3) only expands \Z sequences */
+ n += 2;
+
+ backslash = 0;
+ } else
+ n++;
+
+ /* Did we pass the width barrier? */
+ if (n > ncols) {
+ /*
+ * Work backward to find the first whitespace on-which
+ * bsddialog(3) will wrap the line (but don't go before
+ * the start of this line).
+ */
+ cp = p;
+ while (n > 1 && !isspace(*cp)) {
+ cp--;
+ n--;
+ }
+ if (n > 0 && isspace(*cp))
+ p = cp;
+ wlines++;
+ n = 1;
+ }
+
+ p++;
+ }
+
+ return (wlines);
+}
+
+/*
+ * Free allocated items initialized by tty_maxsize_update()
+ */
+void
+display_maxsize_free(void)
+{
+ if (maxsize != NULL) {
+ free(maxsize);
+ maxsize = NULL;
+ }
+}
diff --git a/lib/libbsdpv/dprompt.h b/lib/libbsdpv/dprompt.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/dprompt.h
@@ -0,0 +1,37 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _DPROMPT_H_
+#define _DPROMPT_H_
+
+#include <sys/cdefs.h>
+
+#include "bsdpv.h"
+
+/* Display characteristics */
+#define ENV_MSG_DONE "msg_done"
+#define ENV_MSG_FAIL "msg_fail"
+#define ENV_MSG_PENDING "msg_pending"
+extern int display_limit;
+extern int label_size;
+extern int pbar_size;
+
+__BEGIN_DECLS
+void dprompt_clear(void);
+void dprompt_dprint(int _fd, const char *_prefix, const char *_append,
+ int _overall);
+void dprompt_draw(const char *_prefix, const char *_append, int _overall);
+void dprompt_gauge_close(void);
+void dprompt_free(void);
+void dprompt_init(struct bsdpv_file_node *_file_list);
+void dprompt_recreate(struct bsdpv_file_node *_file_list,
+ struct bsdpv_file_node *_curfile, int _pct);
+int dprompt_add(const char *_format, ...) __printflike(1, 2);
+int dprompt_sprint(char * restrict _str, const char *_prefix,
+ const char *_append);
+__END_DECLS
+
+#endif /* !_DPROMPT_H_ */
diff --git a/lib/libbsdpv/dprompt.c b/lib/libbsdpv/dprompt.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/dprompt.c
@@ -0,0 +1,738 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/types.h>
+
+#include <err.h>
+#include <libutil.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <bsddialog.h>
+
+#include "bsdpv.h"
+#include "bsdpv_private.h"
+#include "display.h"
+#include "dprompt.h"
+#include "util.h"
+
+#define FLABEL_MAX 1024
+
+static int fheight = 0; /* initialized by dprompt_init() */
+static char dprompt[PROMPT_MAX + 1] = "";
+static char *dprompt_pos = (char *)(0); /* treated numerically */
+
+/* Display characteristics */
+#define FM_DONE 0x01
+#define FM_FAIL 0x02
+#define FM_PEND 0x04
+static uint8_t dprompt_free_mask;
+static char *done = NULL;
+static char *fail = NULL;
+static char *pend = NULL;
+int display_limit = DISPLAY_LIMIT_DEFAULT; /* Max entries to show */
+int label_size = LABEL_SIZE_DEFAULT; /* Max width for labels */
+int pbar_size = PBAR_SIZE_DEFAULT; /* Mini-progressbar size */
+static int gauge_percent = 0;
+static bool gauge_open = false;
+static int done_size, done_lsize, done_rsize;
+static int fail_size, fail_lsize, fail_rsize;
+static int mesg_size, mesg_lsize, mesg_rsize;
+static int pend_size, pend_lsize, pend_rsize;
+static int pct_lsize, pct_rsize;
+#define SPIN_SIZE 4
+static char spin[SPIN_SIZE + 1] = "/-\\|";
+static char msg[PROMPT_MAX + 1];
+static char *spin_cp = spin;
+
+/* Function prototypes */
+static char spin_char(void);
+static int dprompt_add_files(struct bsdpv_file_node *file_list,
+ struct bsdpv_file_node *curfile, int pct);
+
+/*
+ * Returns a pointer to the current spin character in the spin string and
+ * advances the global position to the next character for the next call.
+ */
+static char
+spin_char(void)
+{
+ char ch;
+
+ if (*spin_cp == '\0')
+ spin_cp = spin;
+ ch = *spin_cp;
+
+ /* Advance the spinner to the next char */
+ if (++spin_cp >= (spin + SPIN_SIZE))
+ spin_cp = spin;
+
+ return (ch);
+}
+
+/*
+ * Initialize heights and widths based on various strings and environment
+ * variables (such as ENV_USE_COLOR).
+ */
+void
+dprompt_init(struct bsdpv_file_node *file_list)
+{
+ int len;
+ int max_cols;
+ int max_rows;
+ int nthfile;
+ int numlines;
+ struct bsdpv_file_node *curfile;
+
+ /*
+ * Draw the bsddialog(3) backtitle
+ */
+ if (use_bsddialog && !debug && backtitle != NULL)
+ bsddialog_backtitle(&dconf, backtitle);
+
+ /* Calculate width of the bsddialog(3) gauge box */
+ dwidth = label_size + pbar_size + 9;
+
+ /*
+ * Calculate height of the bsddialog(3) gauge box
+ */
+ dheight = 5;
+ max_rows = display_maxrows();
+ /* adjust max_rows for backtitle, status line, and/or shadow */
+ if (use_bsddialog && !debug)
+ max_rows -= 1;
+ if (backtitle != NULL)
+ max_rows -= use_shadow ? 3 : 2;
+ if (use_bsddialog && use_shadow)
+ max_rows -= 2;
+ /* add lines for `-p text' */
+ numlines = prompt_numlines(pprompt);
+ if (debug)
+ warnx("`-p text' is %i line%s long", numlines,
+ numlines == 1 ? "" : "s");
+ dheight += numlines;
+ /* the implicit trailing newline of `-p text' pads the gauge bar */
+ if (use_bsddialog && pprompt != NULL && *pprompt != '\0')
+ dheight--;
+ /* limit the number of display items (necessary per bsddialog(3)) */
+ if (display_limit == 0 || display_limit > BSDPV_DISPLAY_LIMIT)
+ display_limit = BSDPV_DISPLAY_LIMIT;
+ /* verify fheight will fit (stop if we hit 1) */
+ for (; display_limit > 0; display_limit--) {
+ nthfile = numlines = 0;
+ fheight = (int)bsdpv_nfiles > display_limit ?
+ (unsigned int)display_limit : bsdpv_nfiles;
+ for (curfile = file_list; curfile != NULL;
+ curfile = curfile->next) {
+ nthfile++;
+ numlines += prompt_numlines(curfile->name);
+ if ((nthfile % display_limit) == 0) {
+ if (numlines > fheight)
+ fheight = numlines;
+ numlines = nthfile = 0;
+ }
+ }
+ if (numlines > fheight)
+ fheight = numlines;
+ if ((dheight + fheight + (int)prompt_numlines(aprompt))
+ <= max_rows)
+ break;
+ }
+ /* don't show any items if we run the risk of hitting a blank set */
+ if ((max_rows - (use_shadow ? 5 : 4)) >= fheight)
+ dheight += fheight;
+ else
+ fheight = 0;
+ /* add lines for `-a text' */
+ numlines = prompt_numlines(aprompt);
+ if (debug)
+ warnx("`-a text' is %i line%s long", numlines,
+ numlines == 1 ? "" : "s");
+ dheight += numlines;
+
+ /* For wide mode, long prefix (`pprompt') or append (`aprompt')
+ * strings will bump width */
+ if (wide) {
+ len = (int)prompt_longestline(pprompt);
+ if ((len + 4) > dwidth)
+ dwidth = len + 4;
+ len = (int)prompt_longestline(aprompt);
+ if ((len + 4) > dwidth)
+ dwidth = len + 4;
+ }
+
+ /* Enforce width constraints to maximum values */
+ max_cols = display_maxcols();
+ if (max_cols > 0 && dwidth > max_cols)
+ dwidth = max_cols;
+
+ /* Optimize widths to sane values*/
+ if (pbar_size > dwidth - 9) {
+ pbar_size = dwidth - 9;
+ label_size = 0;
+ /* -9 = "| - [" ... "] |" */
+ }
+ if (pbar_size < 0)
+ label_size = dwidth - 8;
+ /* -8 = "| " ... " - |" */
+ else if (label_size > (dwidth - pbar_size - 9) || wide)
+ label_size = no_labels ? 0 : dwidth - pbar_size - 9;
+ /* -9 = "| " ... " - [" ... "] |" */
+
+ /* Hide labels if requested */
+ if (no_labels)
+ label_size = 0;
+
+ /* Touch up the height (now that we know dwidth) */
+ dheight += prompt_wrappedlines(pprompt, dwidth - 4);
+ dheight += prompt_wrappedlines(aprompt, dwidth - 4);
+
+ if (debug)
+ warnx("dheight = %i dwidth = %i fheight = %i",
+ dheight, dwidth, fheight);
+
+ /* Calculate left/right portions of % */
+ pct_lsize = (pbar_size - 4) / 2; /* -4 == printf("%-3s%%", pct) */
+ pct_rsize = pct_lsize;
+ /* If not evenly divisible by 2, increment the right-side */
+ if ((pct_rsize + pct_rsize + 4) != pbar_size)
+ pct_rsize++;
+
+ /* Initialize "Done" text */
+ if (done == NULL && (done = msg_done) == NULL) {
+ if ((done = getenv(ENV_MSG_DONE)) != NULL)
+ done_size = strlen(done);
+ else {
+ done_size = strlen(BSDPV_DONE_DEFAULT);
+ if ((done = malloc(done_size + 1)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ dprompt_free_mask |= FM_DONE;
+ snprintf(done, done_size + 1, BSDPV_DONE_DEFAULT);
+ }
+ }
+ if (pbar_size < done_size) {
+ done_lsize = done_rsize = 0;
+ *(done + pbar_size) = '\0';
+ done_size = pbar_size;
+ } else {
+ /* Calculate left/right portions for mini-progressbar */
+ done_lsize = (pbar_size - done_size) / 2;
+ done_rsize = done_lsize;
+ /* If not evenly divisible by 2, increment the right-side */
+ if ((done_rsize + done_size + done_lsize) != pbar_size)
+ done_rsize++;
+ }
+
+ /* Initialize "Fail" text */
+ if (fail == NULL && (fail = msg_fail) == NULL) {
+ if ((fail = getenv(ENV_MSG_FAIL)) != NULL)
+ fail_size = strlen(fail);
+ else {
+ fail_size = strlen(BSDPV_FAIL_DEFAULT);
+ if ((fail = malloc(fail_size + 1)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ dprompt_free_mask |= FM_FAIL;
+ snprintf(fail, fail_size + 1, BSDPV_FAIL_DEFAULT);
+ }
+ }
+ if (pbar_size < fail_size) {
+ fail_lsize = fail_rsize = 0;
+ *(fail + pbar_size) = '\0';
+ fail_size = pbar_size;
+ } else {
+ /* Calculate left/right portions for mini-progressbar */
+ fail_lsize = (pbar_size - fail_size) / 2;
+ fail_rsize = fail_lsize;
+ /* If not evenly divisible by 2, increment the right-side */
+ if ((fail_rsize + fail_size + fail_lsize) != pbar_size)
+ fail_rsize++;
+ }
+
+ /* Initialize "Pending" text */
+ if (pend == NULL && (pend = msg_pending) == NULL) {
+ if ((pend = getenv(ENV_MSG_PENDING)) != NULL)
+ pend_size = strlen(pend);
+ else {
+ pend_size = strlen(BSDPV_PENDING_DEFAULT);
+ if ((pend = malloc(pend_size + 1)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ dprompt_free_mask |= FM_PEND;
+ snprintf(pend, pend_size + 1, BSDPV_PENDING_DEFAULT);
+ }
+ }
+ if (pbar_size < pend_size) {
+ pend_lsize = pend_rsize = 0;
+ *(pend + pbar_size) = '\0';
+ pend_size = pbar_size;
+ } else {
+ /* Calculate left/right portions for mini-progressbar */
+ pend_lsize = (pbar_size - pend_size) / 2;
+ pend_rsize = pend_lsize;
+ /* If not evenly divisible by 2, increment the right-side */
+ if ((pend_rsize + pend_lsize + pend_size) != pbar_size)
+ pend_rsize++;
+ }
+
+ if (debug)
+ warnx("label_size = %i pbar_size = %i", label_size, pbar_size);
+
+ dprompt_clear();
+}
+
+/*
+ * Clear the gauge prompt buffer.
+ */
+void
+dprompt_clear(void)
+{
+
+ *dprompt = '\0';
+ dprompt_pos = dprompt;
+}
+
+/*
+ * Append to the gauge prompt buffer. Syntax is like printf(3) and returns the
+ * number of bytes appended to the buffer.
+ */
+int
+dprompt_add(const char *format, ...)
+{
+ int len;
+ va_list ap;
+
+ if (dprompt_pos >= (dprompt + PROMPT_MAX))
+ return (0);
+
+ va_start(ap, format);
+ len = vsnprintf(dprompt_pos, (size_t)(PROMPT_MAX -
+ (dprompt_pos - dprompt)), format, ap);
+ va_end(ap);
+ if (len == -1)
+ errx(EXIT_FAILURE, "%s: Oops, dprompt buffer overflow",
+ __func__);
+
+ if ((dprompt_pos + len) < (dprompt + PROMPT_MAX))
+ dprompt_pos += len;
+ else
+ dprompt_pos = dprompt + PROMPT_MAX;
+
+ return (len);
+}
+
+/*
+ * Append active files to the gauge prompt buffer. Syntax requires a pointer
+ * to the head of the bsdpv_file_node linked-list. Returns the number of files
+ * processed successfully.
+ */
+static int
+dprompt_add_files(struct bsdpv_file_node *file_list,
+ struct bsdpv_file_node *curfile, int pct)
+{
+ char c;
+ char bold_code = 'b'; /* default: enabled */
+ char color_code = '4'; /* default: blue */
+ uint8_t after_curfile = curfile != NULL ? FALSE : TRUE;
+ char *cp;
+ char *lastline;
+ char *name;
+ const char *bg_code;
+ const char *estext;
+ const char *format;
+ enum dprompt_state dstate;
+ int estext_lsize;
+ int estext_rsize;
+ int flabel_size;
+ int hlen;
+ int lsize;
+ int nlines = 0;
+ int nthfile = 0;
+ int pwidth;
+ int rsize;
+ struct bsdpv_file_node *fp;
+ char flabel[FLABEL_MAX + 1];
+ char human[32];
+ char pbar[pbar_size + 16]; /* +15 for optional color */
+ char pbar_cap[sizeof(pbar)];
+ char pbar_fill[sizeof(pbar)];
+
+
+ /* Override color defaults with that of main progress bar */
+ if (use_colors || use_shadow) { /* NB: shadow enables color */
+ color_code = gauge_color[0];
+ /* NB: str[1] aka bg is unused */
+ bold_code = gauge_color[2];
+ if (bold_code == 'b')
+ bold_code = 'B';
+ }
+
+ /*
+ * Create mini-progressbar for current file (if applicable)
+ */
+ *pbar = '\0';
+ if (pbar_size >= 0 && pct >= 0 && curfile != NULL &&
+ (curfile->length >= 0 || display_test)) {
+ unsigned int pbarpct = pct > 100 ? 100 : (unsigned int)pct;
+
+ snprintf(pbar, pbar_size + 1, "%*s%3u%%%*s", pct_lsize, "",
+ pbarpct, pct_rsize, "");
+ if (use_color) {
+ /* Calculate the fill-width of progressbar */
+ pwidth = pbarpct * pbar_size / 100;
+ /* Round up based on one-tenth of a percent */
+ if ((pbarpct * pbar_size % 100) > 50)
+ pwidth++;
+
+ /*
+ * Make two copies of pbar. Make one represent the fill
+ * and the other the remainder (cap). We'll insert the
+ * ANSI delimiter in between.
+ */
+ *pbar_fill = '\0';
+ *pbar_cap = '\0';
+ strncat(pbar_fill, (const char *)(pbar), dwidth);
+ *(pbar_fill + pwidth) = '\0';
+ strncat(pbar_cap, (const char *)(pbar+pwidth), dwidth);
+
+ /* Finalize the mini [color] progressbar */
+ snprintf(pbar, sizeof(pbar),
+ "\\Z%c\\Zr\\Z%c%s%s%s\\Zn", bold_code, color_code,
+ pbar_fill, "\\ZR", pbar_cap);
+ }
+ }
+
+ for (fp = file_list; fp != NULL; fp = fp->next) {
+ flabel_size = label_size;
+ name = fp->name;
+ nthfile++;
+
+ /*
+ * Support multiline filenames (where the filename is taken as
+ * the last line and the text leading up to the last line can
+ * be used as (for example) a heading/separator between files.
+ */
+ nlines += prompt_numlines(name);
+ lastline = prompt_lastline(name);
+ if (name != lastline) {
+ c = *lastline;
+ *lastline = '\0';
+ dprompt_add("%s", name);
+ *lastline = c;
+ name = lastline;
+ }
+
+ /* Support color codes (of bsddialog(3)) in file names */
+ if (use_color) {
+ cp = name;
+ while (*cp != '\0') {
+ if (*cp == '\\' && *(cp + 1) != '\0' &&
+ *(++cp) == 'Z' && *(cp + 1) != '\0') {
+ cp++;
+ flabel_size += 3;
+ }
+ cp++;
+ }
+ if (flabel_size > FLABEL_MAX)
+ flabel_size = FLABEL_MAX;
+ }
+
+ /* If no mini-progressbar, increase label width */
+ if (pbar_size < 0 && flabel_size <= FLABEL_MAX - 2 &&
+ no_labels == FALSE)
+ flabel_size += 2;
+
+ /* If name is too long, add an ellipsis */
+ if (snprintf(flabel, flabel_size + 1, "%s", name) >
+ flabel_size) sprintf(flabel + flabel_size - 3, "...");
+
+ /*
+ * Append the label (processing the current file differently)
+ */
+ if (fp == curfile && pct < 100) {
+ /*
+ * Add an ellipsis to current file name if it will fit.
+ * There may be an ellipsis already from truncating the
+ * label (in which case, we already have one).
+ */
+ cp = flabel + strlen(flabel);
+ if (cp < (flabel + flabel_size))
+ snprintf(cp, flabel_size -
+ (cp - flabel) + 1, "...");
+
+ /* Append label (with spinner and optional color) */
+ dprompt_add("%-*s %c", flabel_size, flabel,
+ spin_char());
+ } else
+ dprompt_add("%-*s%s %s", flabel_size,
+ flabel, use_color ? "\\Zn" : "", " ");
+
+ /*
+ * Append pbar/status (processing the current file differently)
+ */
+ dstate = DPROMPT_NONE;
+ if (fp->msg != NULL)
+ dstate = DPROMPT_CUSTOM_MSG;
+ else if (pbar_size < 0)
+ dstate = DPROMPT_NONE;
+ else if (pbar_size < 4)
+ dstate = DPROMPT_MINIMAL;
+ else if (after_curfile)
+ dstate = DPROMPT_PENDING;
+ else if (fp == curfile) {
+ if (*pbar == '\0') {
+ if (fp->length < 0)
+ dstate = DPROMPT_DETAILS;
+ else if (fp->status == BSDPV_STATUS_RUNNING)
+ dstate = DPROMPT_DETAILS;
+ else
+ dstate = DPROMPT_END_STATE;
+ }
+ else if (display_test) /* status/length ignored */
+ dstate = pct < 100 ?
+ DPROMPT_PBAR : DPROMPT_END_STATE;
+ else if (fp->status == BSDPV_STATUS_RUNNING)
+ dstate = fp->length < 0 ?
+ DPROMPT_DETAILS : DPROMPT_PBAR;
+ else /* not running */
+ dstate = fp->length < 0 ?
+ DPROMPT_DETAILS : DPROMPT_END_STATE;
+ } else { /* before curfile */
+ if (display_test)
+ dstate = DPROMPT_END_STATE;
+ else
+ dstate = fp->length < 0 ?
+ DPROMPT_DETAILS : DPROMPT_END_STATE;
+ }
+ format = use_color ?
+ " [\\Z%c%s%-*s%s%-*s\\ZR\\Zn]\\n" :
+ " [%-*s%s%-*s]\\n";
+ if (fp->status == BSDPV_STATUS_FAILED) {
+ bg_code = "\\Zr\\Z1"; /* Red */
+ estext_lsize = fail_lsize;
+ estext_rsize = fail_rsize;
+ estext = fail;
+ } else { /* e.g., BSDPV_STATUS_DONE */
+ bg_code = "\\Zr\\Z2"; /* Green */
+ estext_lsize = done_lsize;
+ estext_rsize = done_rsize;
+ estext = done;
+ }
+ switch (dstate) {
+ case DPROMPT_PENDING: /* Future file(s) */
+ dprompt_add(" [%-*s%s%-*s]\\n",
+ pend_lsize, "", pend, pend_rsize, "");
+ break;
+ case DPROMPT_PBAR: /* Current file */
+ dprompt_add(" [%s]\\n", pbar);
+ break;
+ case DPROMPT_END_STATE: /* Past/Current file(s) */
+ if (use_color)
+ dprompt_add(format, bold_code, bg_code,
+ estext_lsize, "", estext,
+ estext_rsize, "");
+ else
+ dprompt_add(format,
+ estext_lsize, "", estext,
+ estext_rsize, "");
+ break;
+ case DPROMPT_DETAILS: /* Past/Current file(s) */
+ humanize_number(human, pbar_size + 2, fp->read, "",
+ HN_AUTOSCALE, HN_NOSPACE | HN_DIVISOR_1000);
+
+ /* Calculate center alignment */
+ hlen = (int)strlen(human);
+ lsize = (pbar_size - hlen) / 2;
+ rsize = lsize;
+ if ((lsize+hlen+rsize) != pbar_size)
+ rsize++;
+
+ if (use_color)
+ dprompt_add(format, bold_code, bg_code,
+ lsize, "", human, rsize, "");
+ else
+ dprompt_add(format,
+ lsize, "", human, rsize, "");
+ break;
+ case DPROMPT_CUSTOM_MSG: /* File-specific message override */
+ snprintf(msg, PROMPT_MAX + 1, "%s", fp->msg);
+ if (pbar_size < (mesg_size = strlen(msg))) {
+ mesg_lsize = mesg_rsize = 0;
+ *(msg + pbar_size) = '\0';
+ mesg_size = pbar_size;
+ } else {
+ mesg_lsize = (pbar_size - mesg_size) / 2;
+ mesg_rsize = mesg_lsize;
+ if ((mesg_rsize + mesg_size + mesg_lsize)
+ != pbar_size)
+ mesg_rsize++;
+ }
+ if (use_color)
+ dprompt_add(format, bold_code, bg_code,
+ mesg_lsize, "", msg, mesg_rsize, "");
+ else
+ dprompt_add(format, mesg_lsize, "", msg,
+ mesg_rsize, "");
+ break;
+ case DPROMPT_MINIMAL: /* Short progress bar, minimal room */
+ if (use_color)
+ dprompt_add(format, bold_code, bg_code,
+ pbar_size, "", "", 0, "");
+ else
+ dprompt_add(format, pbar_size, "", "", 0, "");
+ break;
+ case DPROMPT_NONE: /* pbar_size < 0 */
+ /* FALLTHROUGH */
+ default:
+ dprompt_add(" \\n");
+ /*
+ * NB: Leading space required for the case when
+ * spin_char() returns a single backslash [\] which
+ * without the space, changes the meaning of `\n'
+ */
+ }
+
+ /* Stop building if we've hit the internal limit */
+ if (nthfile >= display_limit)
+ break;
+
+ /* If this is the current file, all others are pending */
+ if (fp == curfile)
+ after_curfile = TRUE;
+ }
+
+ /*
+ * Pad the prompt height so that the `-a text' always appears in the
+ * same spot.
+ *
+ * NOTE: fheight is calculated in dprompt_init(). It represents the
+ * maximum height required to display the set of items (broken up into
+ * pieces of display_limit chunks) whose names contain the most
+ * newlines for any given set.
+ */
+ while (nlines < fheight) {
+ dprompt_add("\n");
+ nlines++;
+ }
+
+ return (nthfile);
+}
+
+/*
+ * Process the bsdpv_file_node linked-list of named files, re-generating the
+ * gauge prompt text for the current state of transfers.
+ */
+void
+dprompt_recreate(struct bsdpv_file_node *file_list,
+ struct bsdpv_file_node *curfile, int pct)
+{
+
+ /*
+ * Re-Build the prompt text
+ */
+ dprompt_clear();
+ if (display_limit > 0)
+ dprompt_add_files(file_list, curfile, pct);
+
+ /* bsddialog(3) requires literal newlines */
+ if (use_bsddialog)
+ strexpandnl(dprompt);
+}
+
+/*
+ * Print the gauge prompt text to a buffer.
+ */
+int
+dprompt_sprint(char * restrict str, const char *prefix, const char *append)
+{
+
+ return (snprintf(str, PROMPT_MAX, "%s%s%s%s", use_color ? "\\Zn" : "",
+ prefix ? prefix : "", dprompt, append ? append : ""));
+}
+
+/*
+ * Print the gauge prompt text to file descriptor fd (e.g., STDOUT_FILENO).
+ * Output is suitable for consumption by `bsddialog --gauge'.
+ */
+void
+dprompt_dprint(int fd, const char *prefix, const char *append, int overall)
+{
+ int percent = gauge_percent;
+
+ if (overall >= 0)
+ gauge_percent = percent = overall > 100 ? 100 : overall;
+ dprintf(fd, "XXX\n%s%s%s%s\nXXX\n%i\n", use_color ? "\\Zn" : "",
+ prefix ? prefix : "", dprompt, append ? append : "", percent);
+ fsync(fd);
+}
+
+/*
+ * Draw the gauge prompt text using bsddialog(3).
+ */
+void
+dprompt_draw(const char *prefix, const char *append, int overall)
+{
+ int percent = gauge_percent;
+ int rv;
+ char buf[BSDPV_PPROMPT_MAX + BSDPV_APROMPT_MAX +
+ BSDPV_DISPLAY_LIMIT * 1024];
+
+ dprompt_sprint(buf, prefix, append);
+
+ if (overall >= 0)
+ gauge_percent = percent = overall > 100 ? 100 : overall;
+ if (!gauge_open) {
+ rv = bsddialog_gauge_open(&dconf, buf, dheight, dwidth,
+ (unsigned int)percent);
+ if (rv == BSDDIALOG_OK)
+ gauge_open = true;
+ } else
+ rv = bsddialog_gauge_update(buf, dheight, dwidth,
+ (unsigned int)percent);
+ if (rv != BSDDIALOG_OK)
+ errx(EXIT_FAILURE, "bsddialog gauge: %s",
+ bsddialog_geterror());
+}
+
+/*
+ * Close the persistent gauge widget, if open.
+ */
+void
+dprompt_gauge_close(void)
+{
+
+ if (!gauge_open)
+ return;
+ if (bsddialog_inmode())
+ bsddialog_gauge_close();
+ gauge_open = false;
+}
+
+/*
+ * Free allocated items initialized by dprompt_init()
+ */
+void
+dprompt_free(void)
+{
+
+ dprompt_gauge_close();
+ if ((dprompt_free_mask & FM_DONE) != 0) {
+ dprompt_free_mask ^= FM_DONE;
+ free(done);
+ done = NULL;
+ }
+ if ((dprompt_free_mask & FM_FAIL) != 0) {
+ dprompt_free_mask ^= FM_FAIL;
+ free(fail);
+ fail = NULL;
+ }
+ if ((dprompt_free_mask & FM_PEND) != 0) {
+ dprompt_free_mask ^= FM_PEND;
+ free(pend);
+ pend = NULL;
+ }
+}
diff --git a/lib/libbsdpv/status.h b/lib/libbsdpv/status.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/status.h
@@ -0,0 +1,23 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _STATUS_H_
+#define _STATUS_H_
+
+#include <sys/cdefs.h>
+
+__BEGIN_DECLS
+void status_backdrop_flush(void);
+void status_free(void);
+void status_init(void);
+void status_leave(void);
+void status_leave_no_nl(void);
+void status_printf(const char *_format, ...) __printflike(1, 2);
+void status_restore_tty(void);
+void status_reflush(void);
+__END_DECLS
+
+#endif /* !_STATUS_H_ */
diff --git a/lib/libbsdpv/status.c b/lib/libbsdpv/status.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/status.c
@@ -0,0 +1,238 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <curses.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <bsddialog.h>
+#include <bsddialog_theme.h>
+
+#include "status.h"
+
+/* static globals */
+static char *status_buf = NULL;
+static int status_bufsize = -1;
+static int status_color;
+static int status_row;
+static int status_width;
+static bool backdrop_done;
+static bool status_has_content;
+static char status_sgr[24];
+
+static void
+status_sgr_set(int color)
+{
+ enum bsddialog_color fg, bg;
+ unsigned int flags;
+ int n;
+
+ if (bsddialog_color_attrs(color, &fg, &bg, &flags) != BSDDIALOG_OK) {
+ status_sgr[0] = '\0';
+ return;
+ }
+
+ n = snprintf(status_sgr, sizeof(status_sgr), "\033[3%dm\033[4%dm",
+ (int)fg, (int)bg);
+ if ((flags & BSDDIALOG_BOLD) != 0 &&
+ n > 0 && n < (int)sizeof(status_sgr) - 4)
+ (void)strlcat(status_sgr, "\033[1m", sizeof(status_sgr));
+}
+
+static void
+status_pick_color(void)
+{
+ struct bsddialog_theme theme;
+
+ bsddialog_get_theme(&theme);
+ status_color = theme.screen.color;
+ status_sgr_set(status_color);
+}
+
+/*
+ * Initialize status-line colors after bsddialog(3) init.
+ */
+void
+status_init(void)
+{
+
+ if (!bsddialog_inmode())
+ return;
+
+ backdrop_done = false;
+ status_has_content = false;
+ status_pick_color();
+}
+
+/*
+ * Mark the screen backdrop as handled. bsddialog_gauge_repaint(3) syncs the
+ * backdrop via dialog_backdrop_sync(3) before queueing widget/shadow damage;
+ * touchwin(3)/wnoutrefresh(3) on stdscr(3) here would merge sparse stdscr over
+ * newscr(3) and wipe the L-shaped shadow already queued by the gauge.
+ */
+void
+status_backdrop_flush(void)
+{
+
+ if (!bsddialog_inmode() || backdrop_done)
+ return;
+
+ backdrop_done = true;
+}
+
+/*
+ * Paint the last status message onto stdscr(3) and refresh(3).
+ */
+static void
+status_paint(void)
+{
+
+ if (!bsddialog_inmode() || !status_has_content)
+ return;
+
+ status_row = getmaxy(stdscr) - 1;
+ status_width = getmaxx(stdscr);
+
+ status_pick_color();
+ attrset(status_color);
+ mvaddstr(status_row, 0, status_buf);
+ attrset(A_NORMAL);
+ move(status_row, status_width - 1);
+ refresh();
+}
+
+/*
+ * Repaint the last status message after a gauge-only flush(3).
+ */
+void
+status_reflush(void)
+{
+
+ status_paint();
+}
+
+/*
+ * Print a `one-liner' status message at the bottom of the screen. Messages are
+ * trimmed to fit within the console length (ANSI coloring not accounted for).
+ */
+void
+status_printf(const char *fmt, ...)
+{
+ int n;
+ va_list args;
+
+ if (!bsddialog_inmode())
+ return;
+
+ status_row = getmaxy(stdscr) - 1;
+ status_width = getmaxx(stdscr);
+
+ /* NULL is a special convention meaning "erase the old stuff" */
+ if (fmt == NULL) {
+ move(status_row, 0);
+ clrtoeol();
+ status_has_content = false;
+ refresh();
+ return;
+ }
+
+ /* Resize buffer if terminal width is greater */
+ if ((status_width + 1) > status_bufsize) {
+ status_buf = realloc(status_buf, status_width + 1);
+ if (status_buf == NULL) {
+ status_bufsize = -1;
+ return;
+ }
+ status_bufsize = status_width + 1;
+ }
+
+ /* Print the message within a space-filled buffer */
+ memset(status_buf, ' ', status_width);
+ va_start(args, fmt);
+ n = vsnprintf(status_buf, status_width + 1, fmt, args);
+ va_end(args);
+
+ /* If vsnprintf(3) produced less bytes than the maximum, change the
+ * implicitly-added NUL-terminator into a space and terminate at max */
+ if (n < status_width) {
+ status_buf[n] = ' ';
+ status_buf[status_width] = '\0';
+ }
+
+ status_has_content = true;
+ status_backdrop_flush();
+ status_paint();
+}
+
+/*
+ * Leave the cursor below the status line for the shell prompt.
+ */
+static void
+status_leave_common(bool newline)
+{
+
+ if (!bsddialog_inmode() || !status_has_content)
+ return;
+
+ status_row = getmaxy(stdscr) - 1;
+ status_width = getmaxx(stdscr);
+ move(status_row, status_width - 1);
+ refresh();
+ if (newline) {
+ (void)putchar('\n');
+ (void)fflush(stdout);
+ }
+}
+
+void
+status_leave(void)
+{
+
+ status_leave_common(true);
+}
+
+void
+status_leave_no_nl(void)
+{
+
+ status_leave_common(false);
+}
+
+/*
+ * After endwin(3), rewrite the last status line directly to /dev/tty so it
+ * survives curses teardown (Ctrl-C / abort).
+ */
+void
+status_restore_tty(void)
+{
+
+ if (!status_has_content || status_buf == NULL || status_width <= 0)
+ return;
+
+ printf("\033[%d;1H", status_row + 1);
+ if (status_sgr[0] != '\0')
+ fputs(status_sgr, stdout);
+ (void)fwrite(status_buf, 1, (size_t)status_width, stdout);
+ fputs("\033[0m", stdout);
+ (void)fflush(stdout);
+}
+
+/*
+ * Free allocated items initialized by status_printf()
+ */
+void
+status_free(void)
+{
+
+ if (status_buf != NULL) {
+ free(status_buf);
+ status_buf = NULL;
+ }
+}
diff --git a/lib/libbsdpv/util.h b/lib/libbsdpv/util.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/util.h
@@ -0,0 +1,29 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _UTIL_H_
+#define _UTIL_H_
+
+#include <sys/types.h>
+
+#include <paths.h>
+
+#define SHELL_SPAWN_DEBUG 0 /* Debug spawning of sh(1) commands */
+
+#ifdef _PATH_BSHELL
+#define PATH_SHELL _PATH_BSHELL
+#else
+#define PATH_SHELL "/bin/sh"
+#endif
+
+#define CMDBUFMAX 65536
+
+__BEGIN_DECLS
+int shell_spawn_pipecmd(const char *_cmd, const char *_label, pid_t *_pid);
+void strexpandnl(char *_source);
+__END_DECLS
+
+#endif /* !_UTIL_H_ */
diff --git a/lib/libbsdpv/util.c b/lib/libbsdpv/util.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdpv/util.c
@@ -0,0 +1,113 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <err.h>
+#include <limits.h>
+#include <spawn.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "util.h"
+
+extern char **environ;
+static char cmdbuf[CMDBUFMAX] = "";
+static char shellcmd[PATH_MAX] = PATH_SHELL;
+static char *shellcmd_argv[6] = {
+ shellcmd,
+ __DECONST(char *, "-c"),
+ cmdbuf,
+ __DECONST(char *, "--"),
+ shellcmd,
+ NULL,
+};
+
+/*
+ * Expand escaped newlines (`\n' as two characters) in buffer pointed to by
+ * `source' into literal newlines. The string is modified in-place and will
+ * either shorten or stay the same length.
+ */
+void
+strexpandnl(char *source)
+{
+ uint8_t backslash = 0;
+ char *cp1;
+ char *cp2;
+
+ cp1 = cp2 = source;
+ while (*cp2 != '\0') {
+ *cp1 = *cp2;
+ if (*cp2 == '\\')
+ backslash++;
+ else if (*cp2 != 'n')
+ backslash = 0;
+ else if (backslash > 0) {
+ *(--cp1) = (backslash & 1) == 1 ? '\n' : 'n';
+ backslash = 0;
+ }
+ cp1++;
+ cp2++;
+ }
+ *cp1 = *cp2;
+}
+
+/*
+ * Spawn a sh(1) command. Writes the resulting process ID to the pid_t pointed
+ * at by `pid'. Returns a file descriptor (int) suitable for writing data to
+ * the spawned command (data written to file descriptor is seen as standard-in
+ * by the spawned sh(1) command). Returns `-1' if unable to spawn command.
+ *
+ * If cmd contains a single "%s" sequence, replace it with label if non-NULL.
+ */
+int
+shell_spawn_pipecmd(const char *cmd, const char *label, pid_t *pid)
+{
+ int error;
+ int len;
+ posix_spawn_file_actions_t action;
+#if SHELL_SPAWN_DEBUG
+ unsigned int i;
+#endif
+ int stdin_pipe[2] = { -1, -1 };
+
+ /* Populate argument array */
+ if (label != NULL && fmtcheck(cmd, "%s") == cmd)
+ len = snprintf(cmdbuf, CMDBUFMAX, cmd, label);
+ else
+ len = snprintf(cmdbuf, CMDBUFMAX, "%s", cmd);
+ if (len >= CMDBUFMAX) {
+ warnx("%s:%d:%s: cmdbuf[%u] too small to hold cmd argument",
+ __FILE__, __LINE__, __func__, CMDBUFMAX);
+ return (-1);
+ }
+
+ /* Open a pipe to communicate with the spawned command */
+ if (pipe(stdin_pipe) < 0)
+ err(EXIT_FAILURE, "%s: pipe(2)", __func__);
+
+ /* Fork sh(1) process */
+#if SHELL_SPAWN_DEBUG
+ fprintf(stderr, "%s: spawning `", __func__);
+ for (i = 0; shellcmd_argv[i] != NULL; i++) {
+ if (i == 0)
+ fprintf(stderr, "%s", shellcmd_argv[i]);
+ else if (i == 2)
+ fprintf(stderr, " '%s'", shellcmd_argv[i]);
+ else
+ fprintf(stderr, " %s", shellcmd_argv[i]);
+ }
+ fprintf(stderr, "'\n");
+#endif
+ posix_spawn_file_actions_init(&action);
+ posix_spawn_file_actions_adddup2(&action, stdin_pipe[0], STDIN_FILENO);
+ posix_spawn_file_actions_addclose(&action, stdin_pipe[1]);
+ error = posix_spawnp(pid, shellcmd, &action,
+ (const posix_spawnattr_t *)NULL, shellcmd_argv, environ);
+ if (error != 0) err(EXIT_FAILURE, "%s", shellcmd);
+
+ return stdin_pipe[1];
+}
diff --git a/share/mk/bsd.incs.mk b/share/mk/bsd.incs.mk
--- a/share/mk/bsd.incs.mk
+++ b/share/mk/bsd.incs.mk
@@ -40,6 +40,10 @@
${group}TAG_ARGS= -T ${${group}TAGS:ts,:[*]}
.endif
+install-${group}dir: .PHONY
+ ${INSTALL} -d ${${group}TAG_ARGS} -o ${${group}OWN} -g ${${group}GRP} -m 755 \
+ ${DESTDIR}${${group}DIR}
+
_${group}INCS=
.for header in ${${group}}
.if defined(${group}OWN_${header:T}) || defined(${group}GRP_${header:T}) || \
@@ -62,10 +66,10 @@
stage_includes: stage_as.${header:T}
installincludes: _${group}INS_${header:T}
-_${group}INS_${header:T}: ${header}
+_${group}INS_${header:T}: ${header} install-${group}dir
${INSTALL} ${TAG_ARGS:D${TAG_ARGS},dev} -C -o ${${group}OWN_${.ALLSRC:T}} \
-g ${${group}GRP_${.ALLSRC:T}} -m ${${group}MODE_${.ALLSRC:T}} \
- ${.ALLSRC} \
+ ${.ALLSRC:Ninstall-${group}dir} \
${DESTDIR}${${group}DIR_${.ALLSRC:T}}/${${group}NAME_${.ALLSRC:T}}
.else
_${group}INCS+= ${header}
@@ -76,13 +80,13 @@
stage_includes: stage_files.${group}
installincludes: _${group}INS
-_${group}INS: ${_${group}INCS}
+_${group}INS: ${_${group}INCS} install-${group}dir
.if defined(${group}NAME)
${INSTALL} ${${group}TAG_ARGS} -C -o ${${group}OWN} -g ${${group}GRP} -m ${${group}MODE} \
- ${.ALLSRC} ${DESTDIR}${${group}DIR}/${${group}NAME}
+ ${.ALLSRC:Ninstall-${group}dir} ${DESTDIR}${${group}DIR}/${${group}NAME}
.else
${INSTALL} ${${group}TAG_ARGS} -C -o ${${group}OWN} -g ${${group}GRP} -m ${${group}MODE} \
- ${.ALLSRC} ${DESTDIR}${${group}DIR}/
+ ${.ALLSRC:Ninstall-${group}dir} ${DESTDIR}${${group}DIR}/
.endif
.endif # !empty(_${group}INCS)
diff --git a/share/mk/src.libnames.mk b/share/mk/src.libnames.mk
--- a/share/mk/src.libnames.mk
+++ b/share/mk/src.libnames.mk
@@ -16,6 +16,7 @@
atf_cxx \
auditd \
bsddialog \
+ bsdpv \
bsdstat \
cbor \
devdctl \
@@ -324,6 +325,7 @@
_DP_avl= spl
_DP_be= zfs spl nvpair zfsbootenv
_DP_bsddialog= ncursesw tinfow
+_DP_bsdpv= bsddialog util tinfow ncursesw
.if ${MK_OPENSSL} != "no"
_DP_bsnmp= crypto
.endif
diff --git a/targets/pseudo/userland/Makefile.depend b/targets/pseudo/userland/Makefile.depend
--- a/targets/pseudo/userland/Makefile.depend
+++ b/targets/pseudo/userland/Makefile.depend
@@ -151,6 +151,7 @@
usr.bin/bsdiff/bsdiff \
usr.bin/bsdiff/bspatch \
usr.bin/bsdcat \
+ usr.bin/bsdpv \
usr.bin/bzip2 \
usr.bin/bzip2recover \
usr.bin/c89 \
diff --git a/targets/pseudo/userland/lib/Makefile.depend b/targets/pseudo/userland/lib/Makefile.depend
--- a/targets/pseudo/userland/lib/Makefile.depend
+++ b/targets/pseudo/userland/lib/Makefile.depend
@@ -40,6 +40,7 @@
lib/libblocksruntime \
lib/libbluetooth \
lib/libbsddialog \
+ lib/libbsdpv \
lib/libbsdstat \
lib/libbsm \
lib/libbsnmp/libbsnmp \
diff --git a/usr.bin/Makefile b/usr.bin/Makefile
--- a/usr.bin/Makefile
+++ b/usr.bin/Makefile
@@ -11,6 +11,7 @@
bsdcat \
bsddialog \
bsdiff \
+ bsdpv \
bzip2 \
bzip2recover \
cap_mkdb \
diff --git a/usr.bin/bsdpv/Makefile b/usr.bin/bsdpv/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.bin/bsdpv/Makefile
@@ -0,0 +1,9 @@
+PACKAGE= bsdpv
+
+PROG= bsdpv
+
+CFLAGS+= -I${.CURDIR} -I${SRCTOP}/lib/libbsdpv
+
+LIBADD= bsdpv bsddialog
+
+.include <bsd.prog.mk>
diff --git a/usr.bin/bsdpv/Makefile.depend b/usr.bin/bsdpv/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/usr.bin/bsdpv/Makefile.depend
@@ -0,0 +1,19 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libbsddialog \
+ lib/libbsdpv \
+ lib/libc \
+ lib/libcompiler_rt \
+ lib/msun \
+ lib/ncurses/tinfo \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.bin/bsdpv/bsdpv.1 b/usr.bin/bsdpv/bsdpv.1
new file mode 100644
--- /dev/null
+++ b/usr.bin/bsdpv/bsdpv.1
@@ -0,0 +1,278 @@
+.\"-
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\"
+.Dd July 9, 2026
+.Dt BSDPV 1
+.Os
+.Sh NAME
+.Nm bsdpv
+.Nd stream data from stdin or multiple paths with progress view
+.Sh SYNOPSIS
+.Nm
+.Op options
+.Sm off
+.Op Ar bytes Cm \&:
+.Ar label
+.Sm on
+.Nm
+.Op options
+.Fl m
+.Sm off
+.Op Ar bytes1 Cm \& :
+. Ar label1
+.Sm on
+.Ar path1
+.Oo
+.Sm off
+.Op Ar bytes2 Cm \&:
+.Ar label2
+.Sm on
+.Ar path2
+.Ar ...
+.Oc
+.Sh DESCRIPTION
+.Nm
+provides a progress view, allowing a user to see current throughput rate
+and total data transferred for one or more streams.
+.Pp
+The
+.Nm
+utility has two main modes for processing input.
+.Pp
+The default input mode, without
+.Ql Fl m ,
+.Nm
+reads bytes from standard input.
+A label for the data must be provided.
+.Pp
+The secondary input mode, with
+.Ql Fl m ,
+.Nm
+reads multiple paths
+.Pq up to 2047 or Dq ARG_MAX/2-1 ,
+sequentially.
+.Pp
+Data read in either mode is either thrown away
+.Pq default ,
+sent to a spawned instance of the program specified via
+.Ql Fl x Ar cmd ,
+or sent to a unique file specified by
+.Ql Fl o Ar file .
+.Pp
+With or without
+.Ql Fl m ,
+progress is displayed using
+.Xr bsddialog 3 .
+.Pp
+The following options are available:
+.Bl -tag -width "-b backtitle"
+.It Fl a Ar text
+Display
+.Ar text
+below the file progress indicator(s).
+.It Fl b Ar backtitle
+Display
+.Ar backtitle
+on the backdrop, at top-left, behind the dialog widget.
+.It Fl d
+Debug mode.
+Print gauge prompt data to standard out and provide additional debugging on
+standard error.
+Output is suitable for consumption by
+.Ql bsddialog --gauge
+.Pq see Xr bsddialog 1 .
+.It Fl h
+Produce a short syntax usage with brief option descriptions and exit.
+Output is produced on standard error.
+.It Fl I Ar format
+Customize the multi-file format string used to update the status line.
+Default value
+is
+.Dq Li %'10lli bytes read @ %'9.1f bytes/sec. [%i/%i busy/wait] .
+This format is used when handling more than one file.
+.It Fl i Ar format
+Customize the single-file format string used to update the status line.
+Default value
+is
+.Dq Li %'10lli bytes read @ %'9.1f bytes/sec. .
+This format is used when handling one file.
+.It Fl k
+Keep tite.
+Prevent visually distracting initialization/exit routines for scripts running
+.Nm
+several times.
+.It Fl L Ar size
+Label size.
+If negative, shrink to longest label width.
+.It Fl l
+Line mode.
+Read lines from input instead of bytes.
+.It Fl m
+Multi-input mode.
+Instead of reading bytes from standard input, read from a set of paths
+.Pq one for each label .
+By default, each path is processed sequentially in the order given.
+.It Fl N
+No overrun.
+If enabled, stop reading known-length inputs when input reaches stated length.
+.It Fl n Ar num
+Display at-most
+.Ar num
+progress indicators per screen.
+If zero, display as many as possible.
+If negative, only display the main progress indicator.
+Default is 0.
+Maximum value is 10.
+.It Fl o Ar file
+Output data to
+.Ar file .
+The first occurrence of
+.Ql %s
+.Pq if any
+in
+.Ql Ar file
+will be replaced with the
+.Ar label
+text.
+.It Fl P Ar size
+Mini-progressbar size.
+If negative, don't display mini-progressbars
+.Pq only the large overall progress indicator is shown .
+If zero, auto-adjust based on number of files to read.
+When zero and only one file to read, defaults to -1.
+When zero and more than one file to read, defaults to 17.
+.It Fl p Ar text
+Display
+.Ar text
+above the file progress indicator(s).
+.It Fl T
+Test mode.
+Simulate reading a number of bytes, divided evenly across the number of files,
+while stepping through each percent value of each file to process.
+Appends
+.Dq Li [TEST MODE]
+to the status line
+.Pq to override, use Ql Fl i Ar format or Ql Fl I Ar format .
+No data is actually read.
+.It Fl t Ar title
+Display
+.Ar title
+atop the dialog box.
+.It Fl U Ar num
+Update status line
+.Ar num
+times per-second.
+Default value is
+.Ql Li 2 .
+A value of
+.Ql Li 0
+disables status line updates.
+If negative, update the status line as fast as possible.
+.It Fl w
+Wide mode.
+Allows long
+.Ar text
+arguments used with
+.Ql Fl p
+and
+.Ql Fl a
+to bump the dialog width.
+Prompts wider than the maximum width will wrap.
+.It Fl x Ar cmd
+Execute
+.Ar cmd
+.Pq via Xr sh 1
+and send it data that has been read.
+Data is available to
+.Ar cmd
+on standard input.
+With
+.Ql Fl m ,
+.Ar cmd
+is executed once for each
+.Ar path
+argument.
+The first occurrence of
+.Ql %s
+.Pq if any
+in
+.Ql Ar cmd
+will be replaced with the
+.Ar label
+text.
+.El
+.Sh ENVIRONMENT
+The following environment variables are referenced by
+.Nm :
+.Bl -tag -width ".Ev USE_COLOR"
+.It Ev USE_COLOR
+If set and NULL, disables the use of color.
+.El
+.Sh EXAMPLES
+Simple example to show how fast
+.Xr yes 1
+produces lines
+.Pq usually about ten-million per-second; your results may vary :
+.Bd -literal -offset indent
+yes | bsdpv -l yes
+.Ed
+.Pp
+Display progress while timing how long it takes
+.Xr yes 1
+to produce a half-billion lines
+.Pq usually under one minute; your results may vary :
+.Bd -literal -offset indent
+time yes | bsdpv -Nl 500000000:yes
+.Ed
+.Pp
+An example to watch how quickly a file is transferred using
+.Xr nc 1 :
+.Bd -literal -offset indent
+bsdpv -x "nc -w 1 somewhere.com 3000" -m label file
+.Ed
+.Pp
+A similar example, transferring a file from another process and passing the
+expected size to
+.Nm :
+.Bd -literal -offset indent
+cat file | bsdpv -x "nc -w 1 somewhere.com 3000" 12345:label
+.Ed
+.Pp
+A more complicated example:
+.Bd -literal -offset indent
+tar cf - . | bsdpv -x "gzip -9 > out.tgz" \\
+ $( du -s . | awk '{print $1 * 1024}' ):label
+.Ed
+.Pp
+Taking an image of a disk:
+.Bd -literal -offset indent
+bsdpv -o disk-image.img -m label /dev/ada0
+.Ed
+.Pp
+Writing an image back to a disk:
+.Bd -literal -offset indent
+bsdpv -o /dev/ada0 -m label disk-image.img
+.Ed
+.Pp
+Zeroing a disk:
+.Bd -literal -offset indent
+bsdpv -o /dev/md42 "Zeroing md42" < /dev/zero
+.Ed
+.Sh SEE ALSO
+.Xr bsddialog 1 ,
+.Xr sh 1 ,
+.Xr bsddialog 3 ,
+.Xr bsdpv 3
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 16.0 ,
+derived from the
+.Ql dpv
+utility that first appeared in
+.Fx 10.2 .
+.Sh AUTHORS
+.An Devin Teske Aq dteske@FreeBSD.org
diff --git a/usr.bin/bsdpv/bsdpv.c b/usr.bin/bsdpv/bsdpv.c
new file mode 100644
--- /dev/null
+++ b/usr.bin/bsdpv/bsdpv.c
@@ -0,0 +1,526 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <bsdpv.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdpv_util.h"
+
+/* Debugging */
+static uint8_t debug = FALSE;
+
+/* Data to process */
+static struct bsdpv_file_node *file_list = NULL;
+static unsigned int nfiles = 0;
+
+/* Data processing */
+static uint8_t line_mode = FALSE;
+static uint8_t no_overrun = FALSE;
+static char *buf = NULL;
+static int fd = -1;
+static int output_type = BSDPV_OUTPUT_NONE;
+static size_t bsize;
+static char rpath[PATH_MAX];
+
+/* Extra display information */
+static uint8_t multiple = FALSE; /* `-m' */
+static char *pgm; /* set to argv[0] by main() */
+
+/* Function prototypes */
+static void sig_int(int sig);
+static void usage(void);
+int main(int argc, char *argv[]);
+static int operate_common(struct bsdpv_file_node *file, int out);
+static int operate_on_bytes(struct bsdpv_file_node *file, int out);
+static int operate_on_lines(struct bsdpv_file_node *file, int out);
+
+static int
+operate_common(struct bsdpv_file_node *file, int out)
+{
+ struct stat sb;
+
+ /* Open the file if necessary */
+ if (fd < 0) {
+ if (multiple) {
+ /* Resolve the file path and attempt to open it */
+ if (realpath(file->path, rpath) == 0 ||
+ (fd = open(rpath, O_RDONLY)) < 0) {
+ warn("%s", file->path);
+ file->status = BSDPV_STATUS_FAILED;
+ return (-1);
+ }
+ } else {
+ /* Assume stdin, but if that's a TTY instead use the
+ * highest numbered file descriptor (obtained by
+ * generating new fd and then decrementing).
+ *
+ * NB: /dev/stdin should always be open(2)'able
+ */
+ fd = STDIN_FILENO;
+ if (isatty(fd)) {
+ fd = open("/dev/stdin", O_RDONLY);
+ close(fd--);
+
+ /* This answer might be wrong, if bsdpv(3)
+ * has (by request) opened an output file or
+ * pipe. If we told bsdpv(3) to open a file,
+ * subtract one from previous answer. If
+ * instead we told bsdpv(3) to prepare a pipe
+ * output, subtract two.
+ */
+ switch(output_type) {
+ case BSDPV_OUTPUT_FILE:
+ fd -= 1;
+ break;
+ case BSDPV_OUTPUT_SHELL:
+ fd -= 2;
+ break;
+ }
+ }
+ }
+ }
+
+ /* Allocate buffer if necessary */
+ if (buf == NULL) {
+ /* Use output block size as buffer size if available */
+ if (out >= 0) {
+ if (fstat(out, &sb) != 0) {
+ warn("%i", out);
+ file->status = BSDPV_STATUS_FAILED;
+ return (-1);
+ }
+ if (S_ISREG(sb.st_mode)) {
+ if (sysconf(_SC_PHYS_PAGES) >
+ PHYSPAGES_THRESHOLD)
+ bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
+ else
+ bsize = BUFSIZE_SMALL;
+ } else
+ bsize = MAX(sb.st_blksize,
+ (blksize_t)sysconf(_SC_PAGESIZE));
+ } else
+ bsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
+
+ /* Attempt to allocate */
+ if ((buf = malloc(bsize+1)) == NULL) {
+ bsdpv_end();
+ err(EXIT_FAILURE, "Out of memory?!");
+ }
+ }
+
+ return (0);
+}
+
+static int
+operate_on_bytes(struct bsdpv_file_node *file, int out)
+{
+ int progress;
+ ssize_t r, w;
+
+ if (operate_common(file, out) < 0)
+ return (-1);
+
+ /* [Re-]Fill the buffer */
+ if ((r = read(fd, buf, bsize)) <= 0) {
+ if (fd != STDIN_FILENO)
+ close(fd);
+ fd = -1;
+ file->status = BSDPV_STATUS_DONE;
+ return (100);
+ }
+
+ /* [Re-]Dump the buffer */
+ if (out >= 0) {
+ if ((w = write(out, buf, r)) < 0) {
+ bsdpv_end();
+ err(EXIT_FAILURE, "output");
+ }
+ fsync(out);
+ }
+
+ bsdpv_overall_read += r;
+ file->read += r;
+
+ /* Calculate percentage of completion (if possible) */
+ if (file->length >= 0) {
+ if (file->read >= file->length) {
+ file->status = BSDPV_STATUS_DONE;
+ return (100);
+ }
+ progress = (file->read * 100 / (file->length > 0 ?
+ file->length : 1));
+
+ /* If no_overrun, do not return 100% until read >= length */
+ if (no_overrun && progress == 100 && file->read < file->length)
+ progress--;
+
+ return (progress);
+ } else
+ return (-1);
+}
+
+static int
+operate_on_lines(struct bsdpv_file_node *file, int out)
+{
+ char *p;
+ int progress;
+ ssize_t r, w;
+
+ if (operate_common(file, out) < 0)
+ return (-1);
+
+ /* [Re-]Fill the buffer */
+ if ((r = read(fd, buf, bsize)) <= 0) {
+ if (fd != STDIN_FILENO)
+ close(fd);
+ fd = -1;
+ file->status = BSDPV_STATUS_DONE;
+ return (100);
+ }
+ buf[r] = '\0';
+
+ /* [Re-]Dump the buffer */
+ if (out >= 0) {
+ if ((w = write(out, buf, r)) < 0) {
+ bsdpv_end();
+ err(EXIT_FAILURE, "output");
+ }
+ fsync(out);
+ }
+
+ /* Process the buffer for number of lines */
+ for (p = buf; p != NULL && *p != '\0';)
+ if ((p = strchr(p, '\n')) != NULL)
+ bsdpv_overall_read++, p++, file->read++;
+
+ /* Calculate percentage of completion (if possible) */
+ if (file->length >= 0) {
+ if (file->read >= file->length) {
+ file->status = BSDPV_STATUS_DONE;
+ return (100);
+ }
+ progress = (file->read * 100 / file->length);
+
+ /* If no_overrun, do not return 100% until read >= length */
+ if (no_overrun && progress == 100 && file->read < file->length)
+ progress--;
+
+ return (progress);
+ } else
+ return (-1);
+}
+
+/*
+ * Takes a list of names that are to correspond to input streams coming from
+ * stdin or fifos and produces necessary config to drive the bsdpv(3) gauge
+ * widget. If the `-d' flag is used, output is instead sent to terminal
+ * standard output (and the output can then be saved to a file, piped into
+ * custom `bsddialog --gauge' invocation, or whatever).
+ */
+int
+main(int argc, char *argv[])
+{
+ char dummy;
+ int ch;
+ int n = 0;
+ size_t config_size = sizeof(struct bsdpv_config);
+ size_t file_node_size = sizeof(struct bsdpv_file_node);
+ struct bsdpv_config *config;
+ struct bsdpv_file_node *curfile;
+ struct sigaction act;
+
+ pgm = argv[0]; /* store a copy of invocation name */
+
+ /* Allocate config structure */
+ if ((config = malloc(config_size)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ memset((void *)(config), '\0', config_size);
+
+ /*
+ * Process command-line options
+ */
+ while ((ch = getopt(argc, argv,
+ "a:b:dhi:I:klL:mn:No:p:P:t:TU:wx:")) != -1) {
+ switch(ch) {
+ case 'a': /* additional message text to append */
+ if (config->aprompt == NULL) {
+ config->aprompt = malloc(BSDPV_APROMPT_MAX);
+ if (config->aprompt == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ }
+ snprintf(config->aprompt, BSDPV_APROMPT_MAX, "%s",
+ optarg);
+ break;
+ case 'b': /* bsddialog(3) backtitle */
+ if (config->backtitle != NULL)
+ free((char *)config->backtitle);
+ config->backtitle = malloc(strlen(optarg) + 1);
+ if (config->backtitle == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ *(config->backtitle) = '\0';
+ strcat(config->backtitle, optarg);
+ break;
+ case 'd': /* debugging */
+ debug = TRUE;
+ config->debug = debug;
+ break;
+ case 'h': /* help/usage */
+ usage();
+ break; /* NOTREACHED */
+ case 'i': /* status line format string for single-file */
+ config->status_solo = optarg;
+ break;
+ case 'I': /* status line format string for many-files */
+ config->status_many = optarg;
+ break;
+ case 'k': /* keep tite */
+ config->keep_tite = TRUE;
+ break;
+ case 'l': /* Line mode */
+ line_mode = TRUE;
+ break;
+ case 'L': /* custom label size */
+ config->label_size =
+ (int)strtol(optarg, (char **)NULL, 10);
+ if (config->label_size == 0 && errno == EINVAL)
+ errx(EXIT_FAILURE,
+ "`-L' argument must be numeric");
+ else if (config->label_size < -1)
+ config->label_size = -1;
+ break;
+ case 'm': /* enable multiple file arguments */
+ multiple = TRUE;
+ break;
+ case 'o': /* `-o path' for sending data-read to file */
+ output_type = BSDPV_OUTPUT_FILE;
+ config->output_type = BSDPV_OUTPUT_FILE;
+ config->output = optarg;
+ break;
+ case 'n': /* custom number of files per `page' */
+ config->display_limit =
+ (int)strtol(optarg, (char **)NULL, 10);
+ if (config->display_limit == 0 && errno == EINVAL)
+ errx(EXIT_FAILURE,
+ "`-n' argument must be numeric");
+ else if (config->display_limit < 0)
+ config->display_limit = -1;
+ break;
+ case 'N': /* No overrun (truncate reads of known-length) */
+ no_overrun = TRUE;
+ config->options |= BSDPV_NO_OVERRUN;
+ break;
+ case 'p': /* additional message text to use as prefix */
+ if (config->pprompt == NULL) {
+ config->pprompt =
+ malloc(BSDPV_PPROMPT_MAX + 2);
+ if (config->pprompt == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ /* +2 is for implicit "\n" appended later */
+ }
+ snprintf(config->pprompt, BSDPV_PPROMPT_MAX, "%s",
+ optarg);
+ break;
+ case 'P': /* custom size for mini-progressbar */
+ config->pbar_size =
+ (int)strtol(optarg, (char **)NULL, 10);
+ if (config->pbar_size == 0 && errno == EINVAL)
+ errx(EXIT_FAILURE,
+ "`-P' argument must be numeric");
+ else if (config->pbar_size < -1)
+ config->pbar_size = -1;
+ break;
+ case 't': /* bsddialog(3) title */
+ if (config->title != NULL)
+ free(config->title);
+ config->title = malloc(strlen(optarg) + 1);
+ if (config->title == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ *(config->title) = '\0';
+ strcat(config->title, optarg);
+ break;
+ case 'T': /* test mode (don't read data, fake it) */
+ config->options |= BSDPV_TEST_MODE;
+ break;
+ case 'U': /* updates per second */
+ config->status_updates_per_second =
+ (int)strtol(optarg, (char **)NULL, 10);
+ if (config->status_updates_per_second == 0 &&
+ errno == EINVAL)
+ errx(EXIT_FAILURE,
+ "`-U' argument must be numeric");
+ break;
+ case 'w': /* `-p' and `-a' widths bump display width */
+ config->options |= BSDPV_WIDE_MODE;
+ break;
+ case 'x': /* `-x cmd' for sending data-read to sh(1) code */
+ output_type = BSDPV_OUTPUT_SHELL;
+ config->output_type = BSDPV_OUTPUT_SHELL;
+ config->output = optarg;
+ break;
+ case '?': /* unknown argument (based on optstring) */
+ /* FALLTHROUGH */
+ default: /* unhandled argument (based on switch) */
+ usage();
+ /* NOTREACHED */
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ /* Process remaining arguments as list of names to display */
+ for (curfile = file_list; n < argc; n++) {
+ nfiles++;
+
+ /* Allocate a new struct for the file argument */
+ if (curfile == NULL) {
+ if ((curfile = malloc(file_node_size)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ memset((void *)(curfile), '\0', file_node_size);
+ file_list = curfile;
+ } else {
+ if ((curfile->next = malloc(file_node_size)) == NULL)
+ errx(EXIT_FAILURE, "Out of memory?!");
+ memset((void *)(curfile->next), '\0', file_node_size);
+ curfile = curfile->next;
+ }
+ curfile->name = argv[n];
+
+ /* Read possible `lines:' prefix from label syntax */
+ if (sscanf(curfile->name, "%lli:%c", &(curfile->length),
+ &dummy) == 2)
+ curfile->name = strchr(curfile->name, ':') + 1;
+ else
+ curfile->length = -1;
+
+ /* Read path argument if enabled */
+ if (multiple) {
+ if (++n >= argc)
+ errx(EXIT_FAILURE, "Missing path argument "
+ "for label number %i", nfiles);
+ curfile->path = argv[n];
+ } else
+ break;
+ }
+
+ /* Display usage and exit if not given at least one name */
+ if (nfiles == 0) {
+ warnx("no labels provided");
+ usage();
+ /* NOTREACHED */
+ }
+
+ /*
+ * Set cleanup routine for Ctrl-C action
+ */
+ if (config->display_type == BSDPV_DISPLAY_BSDDIALOG) {
+ act.sa_handler = sig_int;
+ sigaction(SIGINT, &act, 0);
+ }
+
+ /* Set status formats (unless customized with `-i' or `-I') and
+ * action */
+ if (line_mode) {
+ if (config->status_solo == NULL)
+ config->status_solo = LINE_STATUS_SOLO;
+ if (config->status_many == NULL)
+ config->status_many = LINE_STATUS_MANY;
+ config->action = operate_on_lines;
+ } else {
+ if (config->status_solo == NULL)
+ config->status_solo = BYTE_STATUS_SOLO;
+ if (config->status_many == NULL)
+ config->status_many = BYTE_STATUS_MANY;
+ config->action = operate_on_bytes;
+ }
+
+ /*
+ * Hand off to bsdpv(3)...
+ */
+ if (bsdpv(config, file_list) != 0 && debug)
+ warnx("bsdpv(3) returned error!?");
+
+ bsdpv_free();
+
+ exit(EXIT_SUCCESS);
+}
+
+/*
+ * Interrupt handler to indicate we received a Ctrl-C interrupt.
+ */
+static void
+sig_int(int sig __unused)
+{
+ bsdpv_interrupt = TRUE;
+}
+
+/*
+ * Print short usage statement to stderr and exit with error status.
+ */
+static void
+usage(void)
+{
+
+ if (debug) /* No need for usage */
+ exit(EXIT_FAILURE);
+
+ fprintf(stderr, "Usage: %s [options] [bytes:]label\n", pgm);
+ fprintf(stderr, " %s [options] -m [bytes1:]label1 path1 "
+ "[[bytes2:]label2 path2 ...]\n", pgm);
+ fprintf(stderr, "OPTIONS:\n");
+#define OPTFMT "\t%-14s %s\n"
+ fprintf(stderr, OPTFMT, "-a text",
+ "Append text. Displayed below file progress indicators.");
+ fprintf(stderr, OPTFMT, "-b backtitle",
+ "String to be displayed on the backdrop, at top-left.");
+ fprintf(stderr, OPTFMT, "-d",
+ "Debug. Write to standard output instead of bsddialog.");
+ fprintf(stderr, OPTFMT, "-h",
+ "Produce this output on standard error and exit.");
+ fprintf(stderr, OPTFMT, "-I format",
+ "Customize the multi-file status line format.");
+ fprintf(stderr, OPTFMT, "-i format",
+ "Customize the single-file status line format.");
+ fprintf(stderr, OPTFMT, "-k",
+ "Keep tite. Prevent visually distracting exit routines.");
+ fprintf(stderr, OPTFMT, "-L size",
+ "Label size. Must be a number greater than 0, or -1.");
+ fprintf(stderr, OPTFMT, "-l",
+ "Line mode. Read lines from input instead of bytes.");
+ fprintf(stderr, OPTFMT, "-m",
+ "Enable processing of multiple file arguments.");
+ fprintf(stderr, OPTFMT, "-N",
+ "No overrun. Stop reading input at stated length, if any.");
+ fprintf(stderr, OPTFMT, "-n num",
+ "Display at-most num files per screen. Default is -1.");
+ fprintf(stderr, OPTFMT, "-o file",
+ "Output data to file. First %s replaced with label text.");
+ fprintf(stderr, OPTFMT, "-P size",
+ "Mini-progressbar size. Must be a number greater than 3.");
+ fprintf(stderr, OPTFMT, "-p text",
+ "Prefix text. Displayed above file progress indicators.");
+ fprintf(stderr, OPTFMT, "-T",
+ "Test mode. Don't actually read any data, but fake it.");
+ fprintf(stderr, OPTFMT, "-t title",
+ "Title string to be displayed at top of the dialog box.");
+ fprintf(stderr, OPTFMT, "-U num",
+ "Update status line num times per-second. Default is 2.");
+ fprintf(stderr, OPTFMT, "-w",
+ "Wide. Width of `-p' and `-a' text bump dialog width.");
+ fprintf(stderr, OPTFMT, "-x cmd",
+ "Send data to executed cmd. First %s replaced with label.");
+ exit(EXIT_FAILURE);
+}
diff --git a/usr.bin/bsdpv/bsdpv_util.h b/usr.bin/bsdpv/bsdpv_util.h
new file mode 100644
--- /dev/null
+++ b/usr.bin/bsdpv/bsdpv_util.h
@@ -0,0 +1,46 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ */
+
+#ifndef _BSDPV_UTIL_H_
+#define _BSDPV_UTIL_H_
+
+/* Limits */
+#define BUFSIZE_MAX (2 * 1024 * 1024)
+ /* Buffer size for read(2) input */
+#ifndef MAXPHYS
+#define MAXPHYS (128 * 1024)
+ /* max raw I/O transfer size */
+#endif
+
+/*
+ * Memory strategry threshold, in pages: if physmem is larger than this,
+ * use a large buffer.
+ */
+#define PHYSPAGES_THRESHOLD (32 * 1024)
+
+/*
+ * Small (default) buffer size in bytes. It's inefficient for this to be
+ * smaller than MAXPHYS.
+ */
+#define BUFSIZE_SMALL (MAXPHYS)
+
+/*
+ * Math macros
+ */
+#undef MIN
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+#undef MAX
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+
+/*
+ * Extra display information
+ */
+#define BYTE_STATUS_SOLO "%'10lli bytes read @ %'9.1f bytes/sec."
+#define BYTE_STATUS_MANY (BYTE_STATUS_SOLO " [%i/%i busy/wait]")
+#define LINE_STATUS_SOLO "%'10lli lines read @ %'9.1f lines/sec."
+#define LINE_STATUS_MANY (LINE_STATUS_SOLO " [%i/%i busy/wait]")
+
+#endif /* !_BSDPV_UTIL_H_ */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Jul 12, 10:38 PM (11 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34976846
Default Alt Text
D58133.diff (100 KB)
Attached To
Mode
D58133: bsdpv: revive dpv atop bsddialog
Attached
Detach File
Event Timeline
Log In to Comment