+/* -*-c-*-
+ *
+ * Progress bars for terminal programs
+ *
+ * (c) 2022 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <https://www.gnu.org/licenses/>.
+ */
+
#define _XOPEN_SOURCE
+/*----- Header files ------------------------------------------------------*/
+
#include <limits.h>
#include <stdarg.h>
#include <stdlib.h>
#include "multiprogress.h"
+/*----- Progress state lifecycle ------------------------------------------*/
+
static FILE *dup_stream(int fd)
+ /* Return a bidirectional `stdio' stream based on a duplicate of the
+ * file descriptor FD. (That way, we can safely `fclose' the copy
+ * without messing up the original descriptor, e.g., in the case
+ * where FD is standard output.)
+ */
{
FILE *fp;
int newfd;
const char *t;
int n;
+ /* Clear the progress state. */
tty = &progress->tty;
tty->fp = 0;
tty->termbuf = tty->capbuf = 0;
progress->nitems = 0; progress->last_lines = 0;
progress->tv_update.tv_sec = 0; progress->tv_update.tv_usec = 0;
+ /* Determine a suitable terminal. Use a copy of stdout or stderr if they
+ * look like terminals; otherwise, try to open `/dev/tty'. If nothing
+ * works, then give up.
+ */
if (isatty(1)) tty->fp = dup_stream(1);
else if (isatty(2)) tty->fp = dup_stream(2);
else tty->fp = fopen("/dev/tty", "r+");
if (!tty->fp) return (-1);
+ /* Determine the terminal width or height. DIM is `defwd' or `defht'
+ * accordingly; VAR names the environment variable which might have the
+ * relevant dimension; GETCAP is an expression to retrieve the default
+ * dimension from the capability database or return -1 if it's not there,
+ * and DFLT is a fallback default in case the database comes up short.
+ *
+ * Note that we try to use `TIOCGWINSZ' to get the size, so this is all
+ * part of an overly elaborate plan B.
+ */
#define SETDIM(dim, var, getcap, dflt) do { \
t = getenv(var); if (t) { n = atoi(t); if (n) { tty->dim = n; break; } } \
n = getcap; if (n > 0) { tty->dim = n; break; } \
tty->dim = dflt; \
} while (0)
+ /* Determine the terminal capabilities and size. */
#if defined(USE_TERMINFO)
+ /* Use `terminfo'. This is better than `termcap', but not by much. In
+ * particular, there's still a global current terminal-capabilities record
+ * which we'll clobber.
+ */
+ /* Look up the terminal. */
if (setupterm(0, fileno(tty->fp), &err) != OK || err < 1) return (-1);
+ /* Basic cursor motion and erasure. */
tty->cap.cr = tigetstr("cr");
tty->cap.nw = tigetstr("nel");
tty->cap.up = tigetstr("cuu1");
tty->cap.cd = tigetstr("ed");
if (tigetnum("xmc") < 1) {
+ /* No magic cookies, so check on the fancy highlighting. */
+
tty->cap.mr = tigetstr("rev");
tty->cap.md = tigetstr("bold");
tty->cap.me = tigetstr("sgr0");
tty->cap.op = tigetstr("op");
}
+ /* Find out whether erasure uses the current background colour. */
if (tigetflag("bce") > 0) tty->cap.f |= TCF_BCE;
+ /* Set the terminal size. */
SETDIM(defwd, "COLUMNS", tigetnum("co"), 80);
SETDIM(defht, "LINES", tigetnum("li"), 25);
#elif defined(USE_TERMCAP)
+ /* Use `termcap'. This is remarkably awful, really. We must guess at an
+ * upper bound on the size of a termcap string; memory is cheap now, so
+ * I've doubled the traditional size here. `tgetent' establishes a global
+ * current capability string, so we've clobbered that. And
+ */
+ /* Look up the terminal. */
tty->termbuf = malloc(4096); if (!tty->termbuf) return (-1);
tty->capbuf = malloc(4096); if (!tty->capbuf) return (-1);
-
term = getenv("TERM"); if (!term) return (-1);
if (tgetent(tty->termbuf, term) < 1) return (-1);
-
capcur = tty->capbuf;
+
+ /* Basic cursor motion and erasure. */
tty->cap.cr = tgetstr("cr", &capcur);
tty->cap.nw = tgetstr("nw", &capcur);
tty->cap.up = tgetstr("up", &capcur);
tty->cap.cd = tgetstr("cd", &capcur);
if (tgetnum("sg") < 1) {
+ /* No magic cookies, so check on the fancy highlighting. */
+
tty->cap.mr = tgetstr("mr", &capcur);
tty->cap.md = tgetstr("md", &capcur);
tty->cap.me = tgetstr("me", &capcur);
tty->cap.op = tgetstr("op", &capcur);
}
+ /* Find out whether erasure uses the current background colour. */
if (tgetflag("ut") > 0) tty->cap.f |= TCF_BCE;
+ /* Set the pad character correctly. */
t = tgetstr("pc", &capcur); tty->cap.pc = t ? *t : 0;
+ /* Set the terminal size. */
SETDIM(defwd, "COLUMNS", tgetnum("co"), 80);
SETDIM(defht, "LINES", tgetnum("li"), 25);
#else
+ /* Nothing to do. Take a wild guess at the terminal size. */
SETDIM(defwd, "COLUMNS", -1, 80);
SETDIM(defht, "LINES", -1, 25);
#undef SETDIM
+ /* Fill in default motion. These are frequently omitted from capability
+ * strings.
+ */
if (!tty->cap.cr) tty->cap.cr = "\r";
if (!tty->cap.nw) tty->cap.nw = "\r\n";
+
+ /* If the terminal can't do the necessary motion and erasure then give up
+ * on it.
+ */
if (!tty->cap.up || !tty->cap.ce || !tty->cap.cd)
{ fclose(tty->fp); tty->fp = 0; return (-1); }
+
+ /* If the terminal can't do all of the colour stuff we want, then clear
+ * `op' as a hint.
+ */
if (!tty->cap.af || !tty->cap.ab || !tty->cap.op) tty->cap.op = 0;
+
+ /* If the terminal can't return to normal, then clear bold and
+ * reverse-video.
+ */
if (!tty->cap.me) tty->cap.mr = tty->cap.md = 0;
+
+ /* Turn on full buffering. We take responsibility for forcing output at
+ * the right times.
+ */
setvbuf(tty->fp, 0, _IOFBF, BUFSIZ);
+
+ /* All done. */
return (0);
}
free(tty->capbuf); tty->capbuf = 0;
}
+/*----- Active item list maintenance --------------------------------------*/
+
int progress_additem(struct progress_state *progress,
struct progress_item *item)
{
return (0);
}
+/*----- Render state lifecycle --------------------------------------------*/
+
static void setup_render_state(struct progress_state *progress,
struct progress_render_state *render)
{
const struct progress_ttyinfo *tty = &progress->tty;
struct winsize wsz;
+ /* Clear everything. */
render->tty = tty;
render->linebuf = 0; render->linesz = 0;
render->tempbuf = 0; render->tempsz = 0;
#ifdef USE_TERMCAP
+ /* Save old `termcap' globals. We'll restore them in `free_render_state'.
+ */
render->old_bc = BC; BC = 0;
render->old_up = UP; UP = 0;
render->old_pc = PC; PC = tty->cap.pc;
#endif
+ /* Determine the actual terminal size. Fall back on the default we
+ * established in `progress_init' if the kernel doesn't know.
+ */
if (!ioctl(fileno(tty->fp), TIOCGWINSZ, &wsz))
{ render->width = wsz.ws_col; render->height = wsz.ws_row; }
else
{ render->width = tty->defwd; render->height = tty->defht; }
+ /* We'll render progress bars with colour or standout if we can; otherwise,
+ * we'll just insert a `|' in the right place, but that takes up an extra
+ * column, so deduct one from the terminal's width to compensate.
+ */
if (render->width && !tty->cap.op && !tty->cap.mr) render->width--;
}
static void free_render_state(struct progress_render_state *render)
{
+ /* Send accumulated output to the terminal. */
fflush(render->tty->fp);
+
+ /* Free the buffers. */
free(render->linebuf); render->linebuf = 0; render->linesz = 0;
free(render->tempbuf); render->tempbuf = 0; render->tempsz = 0;
+
#ifdef USE_TERMCAP
+ /* Restore the `termcap' globals. */
UP = render->old_up;
BC = render->old_bc;
PC = render->old_pc;
#endif
}
+/*----- Measuring string widths -------------------------------------------*/
+
#define CONV_MORE ((size_t)-2)
#define CONV_BAD ((size_t)-1)
struct measure {
- mbstate_t ps;
- const char *p; size_t i, sz;
- unsigned wd;
+ mbstate_t ps; /* conversion state */
+ const char *p; size_t i, sz; /* input string, and cursor */
+ unsigned wd; /* width accumulated so far */
};
static void init_measure(struct measure *m, const char *p, size_t sz)
+ /* Set up M to measure the SZ-byte string P. */
{
m->p = p; m->sz = sz; m->i = 0; m->wd = 0;
memset(&m->ps, 0, sizeof(m->ps));
}
static int advance_measure(struct measure *m)
+ /* Advance the measurement in M by one character. Return zero if the
+ * end of the string has been reached, or nonzero if there is more to
+ * come.
+ */
{
wchar_t wch;
unsigned chwd;
size_t n;
+ /* Determine the next character's code WCH, the length N of its encoding in P
+ * in bytes, and the character's width CHWD in columns.
+ */
n = mbrtowc(&wch, m->p + m->i, m->sz - m->i, &m->ps);
if (!n) { chwd = 0; n = m->sz - m->i; }
else if (n == CONV_MORE) { chwd = 2; n = m->sz - m->i; }
else if (n == CONV_BAD) { chwd = 2; n = 1; }
else chwd = wcwidth(wch);
+ /* Advance the state. */
m->i += n; m->wd += chwd;
+
+ /* Report whether there's more to come. */
return (m->i < m->sz);
}
static unsigned string_width(const char *p, size_t sz)
+ /* Return the width of the SZ-byte string P, in terminal columns. */
{
struct measure m;
static size_t split_string(const char *p, size_t sz,
unsigned *wd_out, unsigned maxwd)
+ /* Return the size, in bytes, of the shortest prefix of the SZ-byte
+ * string P which is no less than MAXWD columns wide, or SZ if it's
+ * just too short. Store the actual width in *WD_OUT.
+ */
{
struct measure m;
size_t i; unsigned wd;
int more;
init_measure(&m, p, sz);
+
+ /* Advance until we're past the bound. */
for (;;) {
if (!advance_measure(&m)) { *wd_out = m.wd; return (sz); }
if (m.wd >= maxwd) break;
}
+
+ /* Now /continue/ advancing past zero-width characters until we find
+ * something that wasn't zero-width. These might be combining accents or
+ * somesuch, and leaving them off would definitely be wrong.
+ */
wd = m.wd; i = m.i;
for (;;) {
more = advance_measure(&m);
i = m.i;
if (!more) break;
}
+
+ /* All done. */
*wd_out = wd; return (i);
}
+/*----- Output buffer handling --------------------------------------------*/
+
static int grow_linebuf(struct progress_render_state *render, size_t want)
+ /* Extend the line buffer in RENDER so that it's at least WANT bytes
+ * long. Shuffle the accumulated left and right material in the
+ * buffer as necessary. Return 0 on success or -1 if this fails for
+ * any reason.
+ */
{
char *newbuf; size_t newsz;
+ /* Return if there's already enough space. */
if (want <= render->linesz) return (0);
+
+ /* Work out how much space to allocate. The initial size is a rough guess
+ * based on the size of UTF-8 encoded characters, though it's not an upper
+ * bound because many characters have zero width. Double the buffer size
+ * if it's too small. Sneakily insert a terminating zero byte just in
+ * case.
+ */
if (!render->linesz) newsz = 4*render->width + 1;
else newsz = render->linesz;
while (newsz < want) newsz *= 2;
newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
newbuf[newsz] = 0;
+
+ /* Copy the left and right strings into the new buffer. */
if (render->leftsz)
memcpy(newbuf, render->linebuf, render->leftsz);
if (render->rightsz)
memcpy(newbuf + newsz - render->rightsz,
render->linebuf + render->linesz - render->rightsz,
render->rightsz);
+
+ /* Free the old buffer and remember the new one. */
free(render->linebuf); render->linebuf = newbuf; render->linesz = newsz;
+
+ /* All done. */
return (0);
}
static int grow_tempbuf(struct progress_render_state *render, size_t want)
+ /* Extend the temporary buffer in RENDER so that it's at least WANT
+ * bytes long. Anything stored in the buffer will be lost. Return 0
+ * on success or -1 if this fails for any reason.
+ */
{
char *newbuf; size_t newsz;
+ /* Return if there's already enough space. */
if (want <= render->tempsz) return (0);
+
+ /* Work out how much space to allocate. This is the same as `grow_linebuf'
+ * above.
+ */
if (!render->tempsz) newsz = 4*render->width + 1;
else newsz = render->tempsz;
while (newsz < want) newsz *= 2;
newbuf = malloc(newsz + 1); if (!newbuf) return (-1);
newbuf[newsz] = 0;
+
+ /* Free the old buffer and keep the new one. */
free(render->tempbuf); render->tempbuf = newbuf; render->tempsz = newsz;
+
+ /* All done. */
return (0);
}
enum { LEFT, RIGHT };
static int putstr(struct progress_render_state *render, unsigned side,
const char *p, size_t n)
+ /* Add the N-byte string P to SIDE of the line buffer in RENDER.
+ * Return 0 on success or -1 if this fails for any reason.
+ */
{
unsigned newwd = string_width(p, n);
size_t want;
static int vputf(struct progress_render_state *render, unsigned side,
const char *fmt, va_list ap)
+ /* Format a `printf'-style string FMT with arguments AP, and add it
+ * to SIDE of RENDER's line buffer.
+ */
{
va_list bp;
int rc;
return (rc);
}
+/*----- Terminal output ---------------------------------------------------*/
+
#if defined(USE_TERMINFO)
+
static const struct progress_ttyinfo *curtty = 0;
static int putty(int ch) { return (putc(ch, curtty->fp)); }
void progress_put_sequence(const struct progress_ttyinfo *tty,
{ progress_put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
{ progress_put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
+
#elif defined(USE_TERMCAP)
+
static const struct progress_ttyinfo *curtty = 0;
static int putty(int ch) { return (putc(ch, curtty->fp)); }
void progress_put_sequence(const struct progress_ttyinfo *tty,
{ progress_put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); }
void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
{ progress_put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); }
+
#else
+
void progress_put_sequence(const struct progress_ttyinfo *tty,
const char *p, unsigned nlines) { ; }
void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour)
{ ; }
void progress_set_bgcolour(const struct progress_ttyinfo *tty, int colour)
{ ; }
+
#endif
-#define CLRF_ALL 1u
+/*----- Maintaining the progress display ----------------------------------*/
+
+#define CLRF_ALL 1u /* clear everything */
static void clear_progress(struct progress_state *progress,
struct progress_render_state *render, unsigned f)
+ /* Clear the current progress display maintained by PROGRESS,
+ * assisted by the RENDER state.
+ *
+ * If `CLRF_ALL' is set in F, then clear the entire display.
+ * Otherwise, clear the bottom few lines if there are now fewer
+ * progress items than there were last time we rendered the display,
+ * and leave the cursor at the start of the top line ready to
+ * overwrite it.
+ */
{
const struct progress_ttyinfo *tty = &progress->tty;
unsigned ndel, nleave;
progress_put_sequence(tty, tty->cap.cr, 1);
if (progress->last_lines) {
+
+ /* Decide how many lines to delete. Set `ndel' to the number of lines
+ * that will be entirely erased, and `nleave' to the number that we'll
+ * leave.
+ */
if (f&CLRF_ALL)
{ ndel = progress->last_lines; nleave = 0; }
else {
else ndel = progress->last_lines - progress->nitems;
nleave = progress->last_lines - ndel;
}
+
+ /* Now actually do the clearing. Remember that the cursor is still on
+ * the last line.
+ */
if (!ndel)
for (i = 1; i < nleave; i++)
progress_put_sequence(tty, tty->cap.up, 1);
progress_put_sequence(tty, tty->cap.up, 1);
}
}
+
+ /* Remember that we're now at the top of the display. */
progress->last_lines = 0;
}
#define f_any 1u
if (!progress->tty.fp) return (-1);
+
setup_render_state(progress, &render);
clear_progress(progress, &render, 0);
return (0);
}
+/*----- Rendering progress bars -------------------------------------------*/
+
+/* The basic problem here is to render text, formed of several pieces, to the
+ * terminal, placing some marker in the middle of it to indicate how much
+ * progress has been made. This marker might be a colour change, switching
+ * off reverse-video mode, or a `|' character.
+ */
+
enum {
LEFT_COLOUR,
LEFT_MONO,
};
struct bar_state {
- const struct progress_render_state *render;
- unsigned pos, nextpos, state;
+ /* State to track progress through the output of a progress bar, so
+ * that we insert the marker in the right place.
+ *
+ * This is a little state machine. We remember the current column
+ * position, the current state, and the column at which we'll next
+ * change state.
+ */
+
+ const struct progress_render_state *render; /* render state */
+ unsigned pos, nextpos, state; /* as described */
};
static void advance_bar_state(struct bar_state *bar)
+ /* If we've reached the column position for the next state change
+ * then arrange to do whatever it is we're meant to do, and update
+ * for the next change.
+ */
{
const struct progress_render_state *render = bar->render;
const struct progress_ttyinfo *tty = render->tty;
}
}
+/* Little utilities to output a chunk of text, or some spaces. */
static void put_str(FILE *fp, const char *p, size_t sz)
{ while (sz--) putc(*p++, fp); }
static void put_spc(FILE *fp, unsigned n)
{ while (n--) putc(' ', fp); }
static void put_barstr(struct bar_state *bar, const char *p, size_t sz)
+ /* Output the SZ-byte string P, driving the state machine BAR as we
+ * go.
+ */
{
unsigned wd;
size_t n;
for (;;) {
+ /* Main loop. Determine how much space there is to the next state
+ * change, cut off that amount of space from the string, and advance.
+ */
+
n = split_string(p, sz, &wd, bar->nextpos - bar->pos);
if (n == sz && wd < bar->nextpos - bar->pos) break;
put_str(bar->render->tty->fp, p, n); bar->pos += wd;
advance_bar_state(bar);
p += n; sz -= n;
}
+
+ /* Write out the rest of the string, and update the position. We know that
+ * this won't reach the next transition.
+ */
put_str(bar->render->tty->fp, p, sz); bar->pos += wd;
}
static void put_barspc(struct bar_state *bar, unsigned n)
+ /* Output N spaces, driving the state machine BAR as we go. */
{
unsigned step;
const struct progress_ttyinfo *tty = render->tty;
struct bar_state bar;
+ /* If there's no terminal, then there's nothing to do. */
if (!tty->fp) return (-1);
+ /* Set up the render state, with a transition where the bar should end. */
bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5;
+ /* Set the initial state for the render. */
if (tty->cap.op) {
+ /* We have colour. The foreground will always be black. If we've made
+ * negligible progress then advance the state machine immediately, which
+ * will set a yellow background for the remainder of the line; otherwise
+ * set the background green for the start of the bar.
+
+ */
progress_set_fgcolour(tty, 0); bar.state = LEFT_COLOUR;
if (bar.nextpos) progress_set_bgcolour(tty, 2);
else advance_bar_state(&bar);
} else if (tty->cap.mr) {
+ /* We have reverse-video. Write the progress bar in reverse and the rest
+ * in normal.
+ */
+
if (bar.nextpos)
{ bar.state = LEFT_MONO; progress_put_sequence(tty, tty->cap.mr, 1); }
else
{ bar.state = RIGHT; bar.nextpos = render->width; }
} else
+ /* Nothing fancy. We'll write `|' at the right place. */
bar.state = LEFT_SIMPLE;
+ /* Write the left string, spaces to fill the gap, and the right string. */
put_barstr(&bar, render->linebuf, render->leftsz);
put_barspc(&bar, render->width - render->leftwd - render->rightwd);
put_barstr(&bar,
render->linebuf + render->linesz - render->rightsz,
render->rightsz);
+ /* Final output: turn off fancy highlighting, and colours. */
progress_put_sequence(tty, tty->cap.me, 1);
progress_put_sequence(tty, tty->cap.op, 1);
+ /* All done. */
return (0);
}
{
const struct progress_ttyinfo *tty = render->tty;
+ /* If there's no terminal, then there's nothing to do. */
if (!tty->fp) return (-1);
- if (tty->cap.op)
- { progress_set_fgcolour(tty, fg); progress_set_bgcolour(tty, bg); }
- else if (tty->cap.mr)
+ /* Set the general background for the notice. */
+ if (tty->cap.op) {
+ /* We have colours, so set them. */
+
+ progress_set_fgcolour(tty, fg); progress_set_bgcolour(tty, bg);
+ } else if (tty->cap.mr) {
+ /* We have reverse-video, so we might as well use that. */
+
progress_put_sequence(tty, tty->cap.mr, 1);
+ }
+
+ /* Set boldface. (If we have it.) */
progress_put_sequence(tty, tty->cap.md, 1);
+ /* Print the left string. If there's a right string, then print spaces and
+ * the right string; otherwise, try to optimize by erasing to the end of
+ * the line -- if that will erase in the background colour.
+ */
put_str(tty->fp, render->linebuf, render->leftsz);
if (!render->rightsz && (tty->cap.f&TCF_BCE) && tty->cap.ce)
progress_put_sequence(tty, tty->cap.ce, 1);
render->rightsz);
}
+ /* Put things back to normal. */
progress_put_sequence(tty, tty->cap.me, 1);
progress_put_sequence(tty, tty->cap.op, 1);
+ /* All done. */
return (0);
}
+
+/*----- That's all, folks -------------------------------------------------*/
+/* -*-c-*-
+ *
+ * Progress bars for terminal programs
+ *
+ * (c) 2022 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <https://www.gnu.org/licenses/>.
+ */
+
#ifndef MULTIPROGRESS_H
#define MULTIPROGRESS_H
+/*----- Header files ------------------------------------------------------*/
+
#include <stdio.h>
#include <sys/time.h>
+/*----- Data structures ---------------------------------------------------*/
+
struct progress_ttyinfo {
- FILE *fp; /* terminal stream */
+ /* Information about the terminal we're going to write to. This is
+ * maintained as part of the `progress_state' (see below) and
+ * published to renderers as part of the `progress_render_state'.
+ *
+ * The `fp' may be null, if no terminal could be opened, or it's just
+ * too deficient in terms of its capabilities. Capabilities are
+ * named following `termcap' conventions, even though we might well
+ * actually be using `terminfo' instead.
+ */
+
+ FILE *fp; /* terminal stream, or null */
char *termbuf, *capbuf; /* buffers for termcap */
struct { /* terminal capabilities */
unsigned f; /* various flags */
-#define TCF_BCE 1u /* erases to background colour */
- const char *cr, *nw, *up, *ce, *cd; /* cursor motion */
+#define TCF_BCE 1u /* erases to background colour */
+ const char *cr, *nw, *up, *ce, *cd; /* cursor motion and erasure */
const char *mr, *md, *me; /* reverse video, bold */
const char *af, *ab, *op; /* colour */
char pc; /* pad character (termcap) */
{ 0, 0, 0, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 80, 25 }
struct progress_state {
+ /* The main state for progress reporting. Here we keep track of the
+ * items which need to be displayed, and current state of the
+ * display.
+ */
+
struct progress_ttyinfo tty; /* terminal state */
struct progress_item *items, *end_item; /* list of progress items */
unsigned nitems; /* number of items */
#define PROGRESS_STATE_INIT { PROGRESS_TTYINFO_INIT, 0, 0, 0, 0, { 0, 0 } }
struct progress_render_state {
+ /* Information passed to rendering functions.
+ *
+ * The `linebuf' accumulates the text to be shown by
+ * `progress_showbar' or similar, which consists of left and right
+ * portions aligned left and right on the terminal line, with a
+ * variable-size cap in between. These strings are stored at the
+ * beginning and end of the `linebuf', so that (hopefully) new
+ * material can be added in the gap between them without us having to
+ * reallocate the buffer.
+ */
+
const struct progress_ttyinfo *tty; /* terminal state */
unsigned width, height; /* terminal size, in characters */
char *linebuf; size_t linesz; /* output buffer */
};
struct progress_item {
+ /* An item in the progress display.
+ *
+ * The `render' function is passed a pointer to the `progress_item'
+ * structure. Usually, it will need additional state: handle this by
+ * making the `progress_item' be the first member of a larger
+ * structure which holds the necessary information.
+ *
+ * The `render' function should limit its activities to actually
+ * writing a line of information to the terminal. In particular, it
+ * shouldn't try to calculate anything time-dependent itself.
+ */
+
struct progress_state *parent; /* controlling progress state */
struct progress_item *next, *prev; /* forward and backward links */
void (*render)(struct progress_item */*item*/, /* render function */
};
#define PROGRESS_ITEM_INIT { 0, 0, 0, 0 }
+/*----- Functions provided ------------------------------------------------*/
+
extern int progress_init(struct progress_state */*progress*/);
+ /* Initialize PROGRESS.
+ *
+ * It is safe to call this function on uninitialized data. This
+ * involves opening a stream on the terminal, and determining the
+ * terminal's capabilities. Returns zero on success, or -1 on
+ * failure. The structure is usable in either case (though if no
+ * terminal could be opened, then no progress output will be
+ * produced).
+ */
+
extern void progress_free(struct progress_state */*progress*/);
+ /* Free any resources held by PROGRESS.
+ *
+ * It is safe to call this function on a structure that was
+ * initialized to `PROGRESS_STATE_INIT', or by calling
+ * `progress_init', whether that function succeeded or not. It's
+ * also harmless to call it repeatedly on the same structure.
+ */
extern int progress_additem(struct progress_state */*progress*/,
struct progress_item */*item*/);
+ /* If ITEM is already associated with a progress state, then do
+ * nothing and return -1. Otherwise, add ITEM to the end of the list
+ * of active items maintained by PROGRESS, and return 0. The
+ * progress display is not updated.
+ */
extern int progress_removeitem(struct progress_state */*progress*/,
struct progress_item */*item*/);
+ /* If ITEM is not associated with a progress state, then do nothing
+ * and return -1. Otherwise, remove ITEM from the list of active
+ * items maintained by PROGRESS, and return 0. The progress display
+ * is not updated.
+ */
extern int progress_clear(struct progress_state */*progress*/);
+ /* Clear any progress display currently shown on the terminal. Call
+ * this before doing your own output to the terminal, and call
+ * `progress_update' afterwards.
+ */
extern int progress_update(struct progress_state */*progress*/);
+ /* Update the progress display. This will call the `render'
+ * functions for all active progress items to redraw them.
+ */
extern int progress_vputleft(struct progress_render_state */*render*/,
const char */*fmt*/, va_list /*ap*/);
-
extern int progress_vputright(struct progress_render_state */*render*/,
const char */*fmt*/, va_list /*ap*/);
-
__attribute__((format(printf, 2, 3)))
extern int progress_putleft(struct progress_render_state */*render*/,
const char */*fmt*/, ...);
-
__attribute__((format(printf, 2, 3)))
extern int progress_putright(struct progress_render_state */*render*/,
const char */*fmt*/, ...);
+ /* Format the `printf'-style string FMT with the supplied arguments
+ * and add it to the left or right side of the current line being
+ * built up in RENDER. Later strings are added closer to the centre
+ * than earlier strings. If there isn't enough space left to show
+ * the new string on a terminal line, or if there isn't enough memory
+ * for the necessary buffers, then do nothing and return -1. If
+ * everything worked OK, then return 0.
+ */
extern void progress_put_sequence(const struct progress_ttyinfo */*tty*/,
const char */*p*/, unsigned /*nlines*/);
+ /* Send a sequence P -- one of the capability strings from TTY -- to
+ * the terminal TTY, padding it as necessary based on the fact that
+ * NLINES of the display are affected. (See the `tputs' function for
+ * the details.)
+ */
extern void progress_set_fgcolour(const struct progress_ttyinfo */*tty*/,
int /*colour*/);
extern void progress_set_bgcolour(const struct progress_ttyinfo */*tty*/,
int /*colour*/);
+ /* Set COLOUR as the foreground (`set_fgcolour') or background
+ * (`set_bgcolour') colour for subsequent output to TTY.
+ */
extern int progress_showbar(struct progress_render_state */*render*/,
double /*frac*/);
+ /* Show a progress bar. The text of the progress bar will be as
+ * established by the `progress_putleft' and `progress_putright'
+ * functions called on RENDER so far, and the bar will be written to
+ * the terminal associated with RENDER. The length of the bar will
+ * be a FRAC fraction of the width of the terminal, so FRAC should be
+ * a real number between 0.0 and 1.0 inclusive.
+ */
extern int progress_shownotice(struct progress_render_state */*render*/,
int /*bg*/, int /*fg*/);
+ /* Show a notice, i.e., a temporary message which doesn't actually
+ * have any progress associated with it. The text of the notice will
+ * be as established by the `progress_putleft' and
+ * `progress_putright' functions called on RENDER so far, and the
+ * notice will be written to the terminal associated with RENDER.
+ * The notice's background and foreground colours will be BG and FG
+ * respectively.
+ */
+
+/*----- That's all, folks -------------------------------------------------*/
#endif