@@@ dvdrip-upload: change settings while i'm stealing someone else's internet
[dvdrip] / multiprogress.c
index 0594f37..1896219 100644 (file)
@@ -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 <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;
@@ -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
+   */
 
-  term = getenv("TERM"); if (!term) return (-1);
-  if (tgetent(tty->termbuf, term) < 1) return (-1);
-
+  /* 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;
 
-  t = tgetstr("pc", &capcur); PC = t ? *t : 0;
+  /* 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,174 +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;
 
@@ -322,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;
@@ -363,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;
@@ -408,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,
@@ -417,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;
@@ -438,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;
 
@@ -477,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);
 }
 
@@ -509,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,
@@ -525,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++;
+  /* 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);
 }
 
-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
-
-  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);
-  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 -------------------------------------------------*/