Page MenuHomeFreeBSD

D58133.diff
No OneTemporary

D58133.diff

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

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)

Event Timeline