X-Git-Url: https://git.distorted.org.uk/~mdw/dvdrip/blobdiff_plain/35951074cd3c77789c39d6e8a7d9eeb0bba7209e..refs/heads/mdw/cleanup:/multiprogress.c diff --git a/multiprogress.c b/multiprogress.c index 4254e13..1896219 100644 --- a/multiprogress.c +++ b/multiprogress.c @@ -1,5 +1,30 @@ +/* -*-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 . + */ + #define _XOPEN_SOURCE +/*----- Header files ------------------------------------------------------*/ + #include #include #include @@ -19,7 +44,14 @@ #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; @@ -41,6 +73,7 @@ int progress_init(struct progress_state *progress) const char *t; int n; + /* Clear the progress state. */ tty = &progress->tty; tty->fp = 0; tty->termbuf = tty->capbuf = 0; @@ -53,21 +86,41 @@ int progress_init(struct progress_state *progress) 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"); @@ -75,6 +128,8 @@ int progress_init(struct progress_state *progress) 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"); @@ -84,20 +139,28 @@ int progress_init(struct progress_state *progress) 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); @@ -105,6 +168,8 @@ int progress_init(struct progress_state *progress) 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); @@ -114,14 +179,18 @@ int progress_init(struct progress_state *progress) 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); @@ -130,12 +199,34 @@ int progress_init(struct progress_state *progress) #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); } @@ -144,176 +235,137 @@ void progress_free(struct progress_state *progress) struct progress_ttyinfo *tty = &progress->tty; if (tty->fp) { fclose(tty->fp); tty->fp = 0; } - free(tty->termbuf); free(tty->capbuf); tty->termbuf = tty->capbuf = 0; + free(tty->termbuf); tty->termbuf = 0; + free(tty->capbuf); tty->capbuf = 0; } -#if defined(USE_TERMINFO) -static const struct progress_ttyinfo *curtty = 0; -static int putty(int ch) { return (putc(ch, curtty->fp)); } -static void put_sequence(const struct progress_ttyinfo *tty, - const char *p, unsigned nlines) - { if (p) { curtty = tty; tputs(p, nlines, putty); } } -static void set_fgcolour(const struct progress_ttyinfo *tty, int colour) - { put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); } -static void set_bgcolour(const struct progress_ttyinfo *tty, int colour) - { 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)); } -static void put_sequence(const struct progress_ttyinfo *tty, - const char *p, unsigned nlines) - { if (p) { curtty = tty; tputs(p, nlines, putty); } } -static void set_fgcolour(const struct progress_ttyinfo *tty, int colour) - { put_sequence(tty, tgoto(tty->cap.af, -1, colour), 1); } -static void set_bgcolour(const struct progress_ttyinfo *tty, int colour) - { put_sequence(tty, tgoto(tty->cap.ab, -1, colour), 1); } -#else -static void put_sequence(const struct progress_ttyinfo *tty, - const char *p, unsigned nlines) { ; } -static void set_fgcolour(const struct progress_ttyinfo *tty, int colour) - { ; } -static void set_bgcolour(const struct progress_ttyinfo *tty, int colour) - { ; } -#endif +/*----- Active item list maintenance --------------------------------------*/ -#define CLRF_ALL 1u -static int clear_progress(struct progress_state *progress, - struct progress_render_state *render, unsigned f) +int progress_additem(struct progress_state *progress, + struct progress_item *item) { - const struct progress_ttyinfo *tty = &progress->tty; - unsigned ndel, nleave; - unsigned i; - - if (!tty->fp) return (-1); + if (item->parent) return (-1); + item->prev = progress->end_item; item->next = 0; + if (progress->end_item) progress->end_item->next = item; + else progress->items = item; + progress->end_item = item; item->parent = progress; + progress->nitems++; - put_sequence(tty, tty->cap.cr, 1); - if (progress->last_lines) { - if (f&CLRF_ALL) - { ndel = progress->last_lines; nleave = 0; } - else { - if (progress->nitems >= progress->last_lines) ndel = 0; - else ndel = progress->last_lines - progress->nitems; - nleave = progress->last_lines - ndel; - } - if (!ndel) - for (i = 0; i < nleave - 1; i++) put_sequence(tty, tty->cap.up, 1); - else { - for (i = 0; i < ndel - 1; i++) put_sequence(tty, tty->cap.up, 1); - put_sequence(tty, tty->cap.cd, ndel); - for (i = 0; i < nleave; i++) put_sequence(tty, tty->cap.up, 1); - } - } - progress->last_lines = 0; - if (ferror(tty->fp)) return (-1); return (0); } -static int grow_linebuf(struct progress_render_state *render, size_t want) +int progress_removeitem(struct progress_state *progress, + struct progress_item *item) { - char *newbuf; size_t newsz; + if (!item->parent) return (-1); + if (item->next) item->next->prev = item->prev; + else (progress->end_item) = item->prev; + if (item->prev) item->prev->next = item->next; + else (progress->items) = item->next; + progress->nitems--; item->parent = 0; - if (want <= render->linesz) return (0); - 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; - 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(render->linebuf); render->linebuf = newbuf; render->linesz = newsz; return (0); } -static int grow_tempbuf(struct progress_render_state *render, size_t want) -{ - char *newbuf; size_t newsz; - - if (want <= render->tempsz) return (0); - 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; - if (render->tempsz) memcpy(newbuf, render->tempbuf, render->tempsz); - free(render->tempbuf); render->tempbuf = newbuf; render->tempsz = newsz; - return (0); -} +/*----- Render state lifecycle --------------------------------------------*/ -static int setup_render_state(struct progress_state *progress, - struct progress_render_state *render) +static void setup_render_state(struct progress_state *progress, + struct progress_render_state *render) { const struct progress_ttyinfo *tty = &progress->tty; struct winsize wsz; - int rc = 0; + /* 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; rc = -1; } + { 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--; - - return (rc); } 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; @@ -324,23 +376,113 @@ static unsigned string_width(const char *p, size_t sz) 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 lasti; unsigned lastwd; + 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 (;;) { - lasti = m.i; lastwd = m.wd; more = advance_measure(&m); - if (m.wd > maxwd) { *wd_out = lastwd; return (lasti); } - else if (!more) { *wd_out = m.wd; return (sz); } + if (m.wd > wd) break; + 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; @@ -365,6 +507,9 @@ static int putstr(struct progress_render_state *render, unsigned side, 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; @@ -410,6 +555,140 @@ int progress_putright(struct progress_render_state *render, 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, + const char *p, unsigned nlines) + { if (p) { curtty = tty; tputs(p, nlines, putty); } } +void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour) + { 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, + const char *p, unsigned nlines) + { if (p) { curtty = tty; tputs(p, nlines, putty); } } +void progress_set_fgcolour(const struct progress_ttyinfo *tty, int colour) + { 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 + +/*----- 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; + unsigned i; + + 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 { + if (progress->nitems >= progress->last_lines) ndel = 0; + 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); + else { + for (i = 1; i < ndel; i++) + progress_put_sequence(tty, tty->cap.up, 1); + progress_put_sequence(tty, tty->cap.cd, ndel); + for (i = 0; i < nleave; i++) + progress_put_sequence(tty, tty->cap.up, 1); + } + } + + /* Remember that we're now at the top of the display. */ + progress->last_lines = 0; +} + +int progress_clear(struct progress_state *progress) +{ + struct progress_render_state render; + + if (!progress->tty.fp) return (-1); + setup_render_state(progress, &render); + clear_progress(progress, &render, CLRF_ALL); + free_render_state(&render); + return (0); +} + +int progress_update(struct progress_state *progress) +{ + struct progress_render_state render; + const struct progress_ttyinfo *tty = &progress->tty; + struct progress_item *item; + unsigned f = 0; +#define f_any 1u + + if (!progress->tty.fp) return (-1); + + setup_render_state(progress, &render); + clear_progress(progress, &render, 0); + + for (item = progress->items; item; item = item->next) { + if (f&f_any) progress_put_sequence(tty, tty->cap.nw, 1); + render.leftsz = render.rightsz = 0; + render.leftwd = render.rightwd = 0; + item->render(item, &render); progress->last_lines++; f |= f_any; + if (progress->last_lines > render.height) break; + } + if (f&f_any) progress_put_sequence(tty, tty->cap.cr, 1); + free_render_state(&render); + 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, @@ -419,20 +698,32 @@ enum { }; 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; size_t here = bar->nextpos; - while (bar->nextpos == here) { + while (bar->nextpos <= here) { switch (bar->state) { - case LEFT_COLOUR: set_bgcolour(tty, 3); goto right; - case LEFT_MONO: put_sequence(tty, tty->cap.me, 1); goto right; + case LEFT_COLOUR: progress_set_bgcolour(tty, 3); goto right; + case LEFT_MONO: progress_put_sequence(tty, tty->cap.me, 1); goto right; case LEFT_SIMPLE: putc('|', tty->fp); goto right; right: bar->state = RIGHT_ANY; bar->nextpos = render->width; break; case RIGHT_ANY: bar->state = STOP; bar->nextpos = UINT_MAX; break; @@ -440,27 +731,40 @@ static void advance_bar_state(struct bar_state *bar) } } +/* 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; @@ -479,31 +783,48 @@ int progress_showbar(struct progress_render_state *render, double frac) 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) { - set_fgcolour(tty, 0); bar.state = LEFT_COLOUR; - if (bar.nextpos) set_bgcolour(tty, 2); + /* 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; put_sequence(tty, tty->cap.mr, 1); } + { 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); - put_sequence(tty, tty->cap.me, 1); - put_sequence(tty, tty->cap.op, 1); + /* 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); } @@ -511,15 +832,30 @@ int progress_shownotice(struct progress_render_state *render, int bg, int fg) { 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) { set_fgcolour(tty, fg); set_bgcolour(tty, bg); } - else if (tty->cap.mr) put_sequence(tty, tty->cap.mr, 1); - if (tty->cap.md) put_sequence(tty, tty->cap.md, 1); + /* 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) - put_sequence(tty, tty->cap.ce, 1); + progress_put_sequence(tty, tty->cap.ce, 1); else { put_spc(tty->fp, render->width - render->leftwd - render->rightwd); put_str(tty->fp, @@ -527,69 +863,12 @@ int progress_shownotice(struct progress_render_state *render, int bg, int fg) render->rightsz); } - put_sequence(tty, tty->cap.me, 1); - put_sequence(tty, tty->cap.op, 1); - - return (0); -} - -int progress_additem(struct progress_state *progress, - struct progress_item *item) -{ - if (item->parent) return (-1); - item->prev = progress->end_item; item->next = 0; - if (progress->end_item) progress->end_item->next = item; - else progress->items = item; - progress->end_item = item; item->parent = progress; - progress->nitems++; - - return (0); -} - -int progress_clear(struct progress_state *progress) -{ - struct progress_render_state render; - - if (!progress->tty.fp) return (-1); - if (setup_render_state(progress, &render)) return (-1); - clear_progress(progress, &render, CLRF_ALL); - free_render_state(&render); - return (0); -} - -int progress_update(struct progress_state *progress) -{ - struct progress_render_state render; - const struct progress_ttyinfo *tty = &progress->tty; - struct progress_item *item; - unsigned f = 0; -#define f_any 1u + /* Put things back to normal. */ + progress_put_sequence(tty, tty->cap.me, 1); + progress_put_sequence(tty, tty->cap.op, 1); - if (!progress->tty.fp) return (-1); - if (setup_render_state(progress, &render)) return (-1); - clear_progress(progress, &render, 0); - - for (item = progress->items; item; item = item->next) { - if (f&f_any) put_sequence(tty, tty->cap.nw, 1); - render.leftsz = render.rightsz = 0; - render.leftwd = render.rightwd = 0; - item->render(item, &render); progress->last_lines++; f |= f_any; - if (progress->last_lines > render.height) break; - } - if (f&f_any) put_sequence(tty, tty->cap.cr, 1); - free_render_state(&render); + /* All done. */ return (0); } -int progress_removeitem(struct progress_state *progress, - struct progress_item *item) -{ - if (!item->parent) return (-1); - if (item->next) item->next->prev = item->prev; - else (progress->end_item) = item->prev; - if (item->prev) item->prev->next = item->next; - else (progress->items) = item->next; - progress->nitems--; item->parent = 0; - - return (0); -} +/*----- That's all, folks -------------------------------------------------*/