chiark / gitweb /
@@@ tty commentary
authorMark Wooding <mdw@distorted.org.uk>
Sat, 26 Apr 2025 23:43:35 +0000 (00:43 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 26 Apr 2025 23:43:35 +0000 (00:43 +0100)
configure.ac
ui/example/progress-test.c
ui/tty.c

index 4d08050549a40deef769f6bb1ef1f836871ed9f8..49f0339e87ec09c50dd9d04a9f6441f82cd2987d 100644 (file)
@@ -79,6 +79,12 @@ AC_CHECK_TYPE([socklen_t], [],
   [AC_INCLUDES_DEFAULT
 @%:@include <sys/socket.h>
 ])
+AC_CHECK_TYPE([speed_t], [],
+  [AC_DEFINE([speed_t], [short],
+    [Define to `short' if <termios.h> does not define])],
+  [AC_INCLUDES_DEFAULT
+@%:@include <termios.h>
+])
 
 dnl Which version of struct msghdr do we have?
 AC_CHECK_MEMBERS([struct msgdr.msg_control],,, [
index 53c7c9f36e107e02c9a9c6e34b658b09dfb6d2f8..f44179dc9dfca23dd7855ae1325b34487473fbfb 100644 (file)
@@ -55,7 +55,7 @@ int main(int argc, char *argv[])
     else
       ttyprogress_removeitem(&progress, &sub);
     ttyprogress_update(&progress);
-    usleep(100000);
+    usleep(30000);
   }
   ttyprogress_removeitem(&progress, &bar);
   ttyprogress_update(&progress);
index b65f6b744b00595319ef697719cdfe5d92185b4c..fc218f15ac119f5228f20ff63e7ead1c179c7e64 100644 (file)
--- a/ui/tty.c
+++ b/ui/tty.c
 #include "str.h"
 #include "tty.h"
 
-/*----- Operations table --------------------------------------------------*/
+/*----- Miscellaneous preliminaries ---------------------------------------*/
+
+/* Buffer size parameters. */
+#define BUFSZ 4096                     /* size of a buffer */
+#define THRESH (BUFSZ/2)               /* threshold for buffering */
 
 /* Incorporate the published control-block structure into our more elaborate
  * object model.
@@ -151,6 +155,8 @@ static PRINTF_LIKE(1, 2) void debug(const char *fmt, ...)
  *
  * Arguments:  @struct tty *tty@ = pointer to terminal control block
  *             @FILE *fp@ = output file stream
+ *             @speed_t *ospeed_out@ = where to put encoded output rate, or
+ *                     null
  *
  * Returns:    ---
  *
@@ -160,9 +166,12 @@ static PRINTF_LIKE(1, 2) void debug(const char *fmt, ...)
  *             Specifically, this fills in the @fpout@, @baud@, @ht@, and
  *             @wd@ slots.  The width and height come from the kernel, or,
  *             failing that, the environment.
+ *
+ *             For `termcap''s benefit, store the system-encoded output baud
+ *             rate in @*ospeed_out@, if it can be found.
  */
 
-static void common_init(struct tty *tty, FILE *fp)
+static void common_init(struct tty *tty, FILE *fp, speed_t *ospeed_out)
 {
   static const struct baudtab { speed_t code; unsigned baud; } baudtab[] = {
   /*
@@ -296,10 +305,9 @@ static void common_init(struct tty *tty, FILE *fp)
    */
   tty->baud = 0; tty->wd = tty->ht = 0;
   if (fp && !tcgetattr(fileno(fp), &c)) {
-    code = cfgetospeed(&c);
+    code = cfgetospeed(&c); if (ospeed_out) *ospeed_out = code;
     for (b = baudtab; b->baud; b++)
-      if (b->code == code) { tty->baud = b->baud; goto found_baud; }
-  found_baud:
+      if (b->code == code) { tty->baud = b->baud; break; }
     tty_resized(tty);
   }
 
@@ -858,7 +866,7 @@ end:
  */
 static const struct gprintf_ops *global_gops; /* output operations ... */
 static void *global_gout;              /* and context, for @caps_putch */
-static char global_buf[4096];          /* a big output buffer */
+static char global_buf[BUFSZ];         /* a big output buffer */
 static size_t global_len;              /* length of buffer used */
 static int global_err;                 /* error latch, zero if all ok */
 static struct tty *global_lock = 0;    /* interlock for global state */
@@ -885,7 +893,7 @@ static int caps_claim(void)
     return (0);
 }
 
-/* --- @caps_claim@, @caps_release@ --- *
+/* --- @caps_release@ --- *
  *
  * Arguments:  @struct tty *tty@ = control block pointer for current lock
  *                     holder
@@ -898,6 +906,24 @@ static int caps_claim(void)
 static void caps_release(struct tty *tty)
   { assert(global_lock == tty); global_lock = 0; }
 
+/* --- @caps_prepout@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer (ignored)
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *
+ * Returns:    ---
+ *
+ * Use:                Prepare output to the given destination.  Check that the
+ *             output buffer is initially empty (i.e., that it was properly
+ *             flushed last time), and make a note of the output
+ *             destination.
+ */
+
+static void caps_prepout(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go)
+  { assert(!global_len); global_gops = gops; global_gout = go; }
+
 /* --- @caps_putch@ --- *
  *
  * Arguments:  @int ch@ = character to write
@@ -910,29 +936,23 @@ static void caps_release(struct tty *tty)
 
 static int caps_putch(int ch)
 {
-  if (global_len >= sizeof(global_buf)) {
-    if (global_gops->putm(global_gout, global_buf, global_len))
-      global_err = -1;
-    global_len = 0;
-  }
-  global_buf[global_len++] = ch;
-  return (0);
-}
+  size_t n;
 
-/* --- @caps_prepout@ --- *
- *
- * Arguments:  @struct tty *tty@ = control block pointer (ignored)
- *             @const struct gprintf_ops *gops, void *go@ = output
- *                     destination
- *
- * Returns:    ---
- *
- * Use:                Prepare output to the given destination.
- */
+  /* By policy, we flush the buffer as soon as it becomes full, so there
+   * should always be space for at least one byte.
+   */
+  n = global_len; assert(n < BUFSZ);
+  global_buf[n++] = ch;
 
-static void caps_prepout(struct tty *tty,
-                        const struct gprintf_ops *gops, void *go)
-  { assert(!global_len); global_gops = gops; global_gout = go; }
+  /* If the buffer is now full, then flush it. */
+  if (n >= BUFSZ) {
+    if (global_gops->putm(global_gout, global_buf, n)) global_err = -1;
+    n = 0;
+  }
+
+  /* Done. */
+  global_len = n; return (0);
+}
 
 /* --- @caps_flush@ --- *
  *
@@ -1085,6 +1105,10 @@ struct tty_capopslots {
   int (*put2i)(struct tty */*tty*/,
               unsigned /*npad*/,
               const char */*cap*/, int /*i0*/, int /*i1*/);
+
+  /* Determine the cost of a capability. */
+  size_t (*cost)(struct tty */*tty*/,
+                unsigned /*npad*/, const char */*cap*/, int /*i0*/);
 };
 #define TTY_CAPOPSPFX TTY_BASEOPSPFX; struct tty_capopslots cap
 struct tty_capops { TTY_CAPOPSPFX; };
@@ -1153,9 +1177,8 @@ static void init_caps(struct tty_caps *t)
 #define COST_BOOLCAP(uix, info, cap_)
 #define COST_INTCAP(uix, info, cap_)
 #define COST_STRCAP(uix, info, cap_)                                   \
-    if (!t->cap.info) t->cap.info##_cost = -1;                         \
-    else t->cap.info##_cost =                                          \
-          strlen(tgoto(t->cap.info, 0, t->cap.colors - 1));
+    t->cap.info##_cost =                                               \
+      ops->cap.cost(&t->tty, 1, t->cap.info, t->cap.colors - 1);
   ATTRCAPS(COST_BOOLCAP, COST_INTCAP, COST_STRCAP)
 #undef COST_BOOLCAP
 #undef COST_INTCAP
@@ -1255,7 +1278,128 @@ static void init_caps(struct tty_caps *t)
 #undef CLEARCAPS
 }
 
-/* Macros for formatting capabilities. */
+/* --- @caps_padchars@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = extended control block pointer
+ *             @unsigned delay@ = tenths of milliseconds required
+ *             @unsigned f@ = flags
+ *
+ * Returns:    The number of padding characters to send.
+ *
+ * Use:                Determine the number of padding characters to send to achieve
+ *             a delay of a given duration.  If @CPF_FORCE@ is set, then
+ *             ignore the @pb@ and @xon@ capabilities.
+ */
+
+#define CPF_FORCE 1u
+static size_t caps_padchars(struct tty *tty, unsigned delay, unsigned f)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+
+  if (!(f&CPF_FORCE) && (t->tty.baud < t->cap.pb || t->cap.xon)) {
+    /* We're not forced to pad, and the baud rate is sufficiently low or we
+     * have flow control, then there's nothing to do.
+     */
+
+    return (0);
+  } else {
+    /* We're transmitting at R b/s, and we want to send N B of data so that
+     * this takes D/10000 s.  We're likely sending either seven bits with
+     * parity or eight bits without, plus one stop bit, per character, so we
+     * must send 9 N b, which will take 9 N/R s = D/10000 s.  Rearranging
+     * gives
+     *
+     *        N = R D/90000 ,
+     *
+     * and we should round upwards.
+     */
+
+    return ((unsigned long)t->tty.baud*delay + 89999/90000);
+  }
+}
+
+#if defined(HAVE_TERMCAP) || defined(HAVE_TERMINFO)
+
+/* --- @caps_cost@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected
+ *             @const char *ctrl@ = formatted control string
+ *
+ * Returns:    A linear `cost' for sending the capability.
+ *
+ * Use:                Determines the cost for a `termcap' or `terminfo' capability
+ *             by parsing a @tputs@-format string.
+ */
+
+static unsigned scan_delay(const char **p_inout, unsigned npad)
+{
+  const char *p = *p_inout;
+  unsigned t, f;
+
+#define f_padmul 1u
+
+  f = 0; t = 0;
+  while (ISDIGIT(*p)) t = 10*t + (*p++ - '0');
+  t *= 10;
+  if (*p == '.') {
+    p++;
+    if (ISDIGIT(*p)) {
+      t += *p++ - '0';
+      while (ISDIGIT(*p)) p++;
+    }
+  }
+  for (;;)
+    switch (*p) {
+      case '*': p++; f |= f_padmul; break;
+      case '/': p++; break;
+      default: goto done;
+    }
+done:
+  if (f&f_padmul) t *= npad;
+  *p_inout = p; return (npad);
+
+#undef f_padmul
+}
+
+static size_t caps_cost(struct tty *tty, unsigned npad, const char *ctrl)
+{
+  size_t n;
+  unsigned t;
+  const char *p;
+
+  p = ctrl;
+  if (ISDIGIT(*p)) {
+    /* The string starts with a number, so it's a `termcap'-style string with
+     * a leading millisecond count.
+     */
+
+    t = scan_delay(&p, npad);
+    n = strlen(p);
+  } else {
+    /* No initial number.  Search for `terminfo'-style %|$<NNN.N[*|/]|%
+     * droppings.
+     */
+
+    n = 0; t = 0;
+    while (*p)
+      if (*p != '$' || p[1] != '<' || (!ISDIGIT(p[2] && p[2] != '>')))
+       { n++; p++; }
+      else {
+       p += 2; t += scan_delay(&p, npad);
+       if (*p == '>') p++;
+      }
+  }
+
+  /* All done. */
+  return (n + caps_padchars(tty, t, CPF_FORCE));
+}
+
+#endif
+
+/* Macros for formatting capabilities.  The @...V@ macros evaluate the cap
+ * name, while the unmarked macros interpret it as a slot name.
+ */
 #define PUT0V(npad, cap_)                                              \
   CHECK(ops->cap.put0(&t->tty, (npad), (cap_)))
 #define PUT1IV(npad, cap_, i0)                                         \
@@ -1267,11 +1411,63 @@ static void init_caps(struct tty_caps *t)
 #define PUT1I(npad, name, i0) PUT1IV(npad, t->cap.name, i0)
 #define PUT2I(npad, name, i0, i1) PUT2IV(npad, t->cap.name, i0, i1)
 
+/* --- @caps_iterate@ --- *
+ *
+ * Arguments:  @struct tty_caps *t@ = extended control block pointer
+ *             @const char *cap1, *capn@ = capability strings for single and
+ *                     multiple operations
+ *             @unsigned f@ = flags
+ *             @unsigned npad@ = number of lines affected
+ *             @unsigned n@ = number of operations to do
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Sends control codes to do some operation @n@ times.
+ *
+ *             The capability string @cap1@ should do the operation once,
+ *             while @capn@ should do it some number of times as requested
+ *             by an argument.  The single-operation cap is likely better
+ *             for a single use.  Either or both capabilities might be
+ *             missing.
+ */
+
+#define CIF_PADMUL 1u
+static int caps_iterate(struct tty_caps *t,
+                       const char *cap1, const char *capn,
+                       unsigned f, unsigned npad, unsigned n)
+{
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  unsigned max, nn;
+  int rc;
+
+  if (cap1 && (n == 1 || !capn))
+    while (n--) PUT0V(npad, cap1);
+  else {
+    max = npad && (f&CIF_PADMUL) ? INT_MAX/npad : INT_MAX;
+    while (n) {
+      nn = n; if (nn > max) nn = max;
+      PUT1IV(npad, capn, nn);
+      n -= nn;
+    }
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
 /* --- @caps_setcolour@ --- *
  *
  * Arguments:  @struct tty_caps *t@ = extended control block pointer
- *             
+ *             @const char *cap@ = capability string to apply (typically
+ *                     %|setaf|% or %|setab|%)
+ *             @uint32 spc, clr@ = the colour space and number
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Emit the correct control string to set the foreground or
+ *             background colour as indicated.
  */
+
 static int caps_setcolour(struct tty_caps *t,
                          const char *cap, uint32 spc, uint32 clr)
 {
@@ -1304,55 +1500,110 @@ end:
   return (rc);
 }
 
+/* --- @caps_setattr_internal@ --- *
+ *
+ * Arguments:  @struct tty_caps *t@ = extended control block pointer
+ *             @const struct tty_attr *a@ = attribute to set, already
+ *                     clamped
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Internal version of @caps_setattr@.  Assumes that @prepout@
+ *             has already been called, and that @flush@ will be called
+ *             later.
+ *
+ *             This is split out from @caps_setattr@ because it's used
+ *             (e.g., by @caps_move@) to restore the capabilities after
+ *             moving the cursor on a terminal which doesn't advertise
+ *             %|msgr|%.
+ */
+
 static int caps_setattr_internal(struct tty_caps *t,
                                 const struct tty_attr *a)
 {
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
-  uint32 diff;
+  uint32 diff, m;
   int rc;
-  unsigned c, z, f = 0;
-#define f_clrall 1u
+  unsigned c, z;
 
-  /* Work out what needs doing. */
+  /* Work out what, if anything, needs doing. */
   diff = a->f ^ t->tty.st.attr.f;
 
-  /* Some terminals might not be able to clear individual attributes that
-   * they can set, and some capabilities for turning attributes on don't even
-   * have a corresponding attribute for turning them off again individually,
-   * so we have to use %|sgr0|% to start from scratch.  Of course, if we need
-   * to do that, we need to restore the other active attributes, so we must
-   * check up front.
+  /* Form a basic strategy.
+   *
+   * In the general case, we're applying some attributes (say bold face) and
+   * cancelling others (say italics).  We likely have a big club for the
+   * latter in the form of the %|sgr0|% capability, which cancels all
+   * attributes and is likely fairly cheap.  On the other hand, some -- but,
+   * infuriatingly, not all -- attributes have a cap string for cancelling
+   * them, which may or may not be present.
+   *
+   * Hence, if we're holding over a lot of attributes from the previous
+   * state, and cancelling just a small number, then it's likely better to
+   * just cancel the individual attributes which need it, because otherwise
+   * we'd have to reapply all of the ones we didn't actually mean to change.
+   *
+   * Add up the costs of each approach and use the cheaper one.  Of course,
+   * if we're forced into one or the other, then we have no choice.
+   *
+   * One point worth noting is that we don't make use of the general %|sgr|%
+   * capability here.  For one thing, %|sgr|% takes nine arguments -- so it
+   * can't be used from `termcap'.  But, more importantly, we don't know, in
+   * any particular case, what it does to other attributes.
    */
+  if (t->cap.sgr0) {
 
-  c = 0;
+    /* First, add up the costs of cancelling the individual attributes which
+     * are no longer wanted.  If we find that we can't cancel one, then skip
+     * ahead.
+     */
+    c = 0;
 #define CLEARP(mask) ((diff&(mask)) && !(a->f&(mask)))
 #define ADDCOST(cap_) do {                                             \
-  if (t->cap.cap_) c += t->cap.cap_##_cost;                            \
-  else f |= f_clrall;                                                  \
+    if (t->cap.cap_) c += t->cap.cap_##_cost;                          \
+    else goto sgr0;                                                    \
 } while (0)
-  if (CLEARP(TTAF_LNMASK)) ADDCOST(rmul);
-  if (CLEARP(TTAF_WTMASK)) f |= f_clrall;
-  if (diff&~a->f&TTAF_INVV) f |= f_clrall;
-  if (diff&~a->f&TTAF_ITAL) ADDCOST(ritm);
-  if (CLEARP(TTAF_FGSPCMASK) || CLEARP(TTAF_BGSPCMASK)) ADDCOST(op);
+    if (CLEARP(TTAF_LNMASK)) ADDCOST(rmul);
+    if (CLEARP(TTAF_WTMASK)) goto sgr0;
+    if (diff&~a->f&TTAF_INVV) goto sgr0;
+    if (diff&~a->f&TTAF_ITAL) ADDCOST(ritm);
+    if (CLEARP(TTAF_FGSPCMASK) || CLEARP(TTAF_BGSPCMASK)) ADDCOST(op);
 #undef CLEARP
 #undef ADDCOST
 
-  z = 0;
-  switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
-    case TTLN_ULINE: z += t->cap.smul_cost; break;
-  }
-  switch ((a->f&TTAF_WTMASK) >> TTAF_WTSHIFT) {
-    case TTWT_BOLD: z += t->cap.bold_cost; break;
-    case TTWT_DIM: z += t->cap.dim_cost; break;
-  }
-  if (a->f&TTAF_INVV) z += t->cap.rev_cost;
-  if (a->f&TTAF_ITAL) z += t->cap.sitm_cost;
-  if (a->f&TTAF_FGSPCMASK) z += t->cap.setaf_cost;
-  if (a->f&TTAF_BGSPCMASK) z += t->cap.setab_cost;
+    /* Now, add up the costs of reapplying all of the attributes which aren't
+     * supposed to change.  (Attributes which are changing don't count here
+     * because we'll have to fiddle with them anyway.)
+     */
+    z = 0;
+    if (!(diff&TTAF_LNMASK))
+      switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+       case TTLN_ULINE: z += t->cap.smul_cost; break;
+      }
+    if (!(diff&TTAF_WTMASK))
+      switch ((a->f&TTAF_WTMASK) >> TTAF_WTSHIFT) {
+       case TTWT_BOLD: z += t->cap.bold_cost; break;
+       case TTWT_DIM: z += t->cap.dim_cost; break;
+      }
+    m = a->f&~diff;
+    if (m&TTAF_INVV) z += t->cap.rev_cost;
+    if (m&TTAF_ITAL) z += t->cap.sitm_cost;
+#define COLOURCOST(G, g, cap_) do {                                    \
+    if (!(diff&TTAF_##G##SPCMASK) &&                                   \
+       (a->f&TTAF_##G##SPCMASK) && a->g == t->tty.st.attr.g)           \
+      z += t->cap.cap_##_cost;                                         \
+} while (0)
+    COLOURCOST(FG, fg, setaf);
+    COLOURCOST(BG, bg, setab);
+#undef COLOURCOST
+
+    if (z + t->cap.sgr0_cost < c) {
+    sgr0:
+      /* We've decided to clear everything and start over. */
 
-  if ((t->cap.sgr0 && z + t->cap.sgr0_cost < c) || (f&f_clrall))
-    { PUT0(0, sgr0); diff = a->f; t->tty.st.attr.fg = t->tty.st.attr.bg; }
+      PUT0(0, sgr0); diff = a->f; t->tty.st.attr.fg = t->tty.st.attr.bg = 0;
+    }
+  }
 
   /* Line style. */
   if (diff&TTAF_LNMASK)
@@ -1406,6 +1657,20 @@ end:
 #undef f_clrall
 }
 
+/* --- @caps_setattr@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @const struct tty_attr *a@ = attribute to set, already
+ *                     clamped
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Arrange to display future characters with the display
+ *             attributes indicated by @a@.
+ */
+
 static int caps_setattr(struct tty *tty,
                        const struct gprintf_ops *gops, void *go,
                        const struct tty_attr *a)
@@ -1414,12 +1679,26 @@ static int caps_setattr(struct tty *tty,
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Hand off to @caps_setattr_internal@. */
   ops->cap.prepout(&t->tty, gops, go);
   rc = caps_setattr_internal(t, a);
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_setmodes@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @uint32 modes_bic, modes_xor@ = masks to apply to the modes
+ *                     settings
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Set the requested terminal modes.
+ */
+
 static int caps_setmodes(struct tty *tty,
                         const struct gprintf_ops *gops, void *go,
                         uint32 modes_bic, uint32 modes_xor)
@@ -1476,29 +1755,19 @@ end:
   t->tty.st.modes = modes; return (rc);
 }
 
-#define CIF_PADMUL 1u
-static int caps_iterate(struct tty_caps *t,
-                       const char *cap1, const char *capn,
-                       unsigned f, unsigned npad, unsigned n)
-{
-  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
-  unsigned max, nn;
-  int rc;
-
-  if (cap1 && (n == 1 || !capn))
-    while (n--) PUT0V(npad, cap1);
-  else {
-    max = npad && (f&CIF_PADMUL) ? INT_MAX/npad : INT_MAX;
-    while (n) {
-      nn = n; if (nn > max) nn = max;
-      PUT1IV(npad, capn, nn);
-      n -= nn;
-    }
-  }
-  rc = 0;
-end:
-  return (rc);
-}
+/* --- @caps_move_relative@ --- *
+ *
+ * Arguments:  @struct tty_caps *t@ = extended control block pointer
+ *             @int delta@ = number of places to move
+ *             @const char *fw1, *fwn, *rv1, *rvn@ = capability strings for
+ *                     to move by a single or multiple places, in the
+ *                     forward (positive) or backwards (negative) direction
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Perform a relative motion, making the most of the supplied
+ *             capabilities.
+ */
 
 static int caps_move_relative(struct tty_caps *t,
                              int delta,
@@ -1513,38 +1782,94 @@ static int caps_move_relative(struct tty_caps *t,
   return (caps_iterate(t, mv1, mvn, 0, 0, delta));
 }
 
+/* --- @caps_move_absx@ --- *
+ *
+ * Arguments:  @struct tty_caps *t@ = extended control block pointer
+ *             @int x@ = column to move the cursor to
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Move the cursor to the given column in the current line.
+ *             This is possible even if we have only relative motion
+ *             because we can use the carriage return.
+ */
+
+static int caps_move_absx(struct tty_caps *t, int x)
+{
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int rc;
+
+  if (t->cap.hpa)
+    PUT1I(1, hpa, x);
+  else {
+    PUT0(1, cr);
+    CHECK(caps_iterate(t, t->cap.cuf1, t->cap.cuf, 0, 1, x));
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
+/* --- @caps_move@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned orig@ = origin
+ *             @int y, x@ = new cursor position
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Move the cursor.
+ */
+
 static int caps_move(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     unsigned orig, int y, int x)
 {
   struct tty_caps *t = (struct tty_caps *)tty;
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
-  struct tty_attr a;
+  struct tty_attr a, aa = TTY_ATTR_INIT;
   int rc;
 
+  /* Check that the arguments are basically sensible. */
+  if ((!(orig&TTOF_YCUR) && y < 0) || (!(orig&TTOF_XCUR) && x < 0))
+    { rc = -1; goto end; }
+
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
 
+  /* If it's unsafe to move with insert mode on, then turn it off. */
   if (!t->cap.mir && (t->tty.st.modes&TTMF_INS)) PUT0(0, rmir);
 
+  /* If it's unsafe to move with attributes set then make a copy of them and
+   * clear them all.
+   */
   a = t->tty.st.attr;
-  if (!t->cap.msgr && a.f) { PUT0(0, sgr0); t->tty.st.attr.f = 0; }
+  if (!t->cap.msgr && a.f) {
+    if (t->cap.sgr0) PUT0(0, sgr0);
+    else CHECK(caps_setattr_internal(t, &aa));
+    t->tty.st.attr.f = 0; t->tty.st.attr.fg = t->tty.st.attr.bg = 0;
+  }
 
+  /* Main dispatch. */
   switch (orig) {
     case TTORG_HOME:
+      /* Fully absolute movement.
+       *
+       * Moving to the home position is likely easy and fast.  Use general
+       * cursor positioning if available.  Otherwise, try to set the vertical
+       * and horizontal positions separately.  If all that fails, move to the
+       * home position and use relative motion.
+       */
+
       if (t->cap.home && !x && !y)
        PUT0(1, home);
       else if (t->cap.cup)
        PUT2I(1, cup, y, x);
       else if (t->cap.vpa) {
        PUT1I(1, vpa, y);
-       if (t->cap.hpa)
-         PUT1I(1, hpa, x);
-       else {
-         PUT0(1, cr);
-         CHECK(caps_move_relative(t, x,
-                                  t->cap.cuf1, t->cap.cuf,
-                                  t->cap.cub1, t->cap.cub));
-       }
+       CHECK(caps_move_absx(t, x));
       } else if (t->cap.home) {
        PUT0(1, home);
        CHECK(caps_iterate(t, t->cap.cud1, t->cap.cud, 0, 1, y));
@@ -1554,6 +1879,8 @@ static int caps_move(struct tty *tty,
       break;
 
     case TTORG_CUR:
+      /* Fully relative movement.  This is easy. */
+
       CHECK(caps_move_relative(t, y,
                               t->cap.cud1, t->cap.cud,
                               t->cap.cuu1, t->cap.cuu));
@@ -1563,22 +1890,28 @@ static int caps_move(struct tty *tty,
       break;
 
     case TTOF_XHOME | TTOF_YCUR:
+      /* Absolute horizontal movement, with relative vertical movement.
+       *
+       * If we want to move to the start of the next line, this is a
+       * `newline' operation.  Otherwise, use two separate motions.
+       */
+
       if (x == 0 && y == 1)
        PUT0(1, nel);
       else {
        CHECK(caps_move_relative(t, y,
                                 t->cap.cud1, t->cap.cud,
                                 t->cap.cuu1, t->cap.cuu));
-       if (t->cap.hpa && x)
-         PUT1I(1, hpa, x);
-       else {
-         PUT0(1, cr);
-         CHECK(caps_iterate(t, t->cap.cuf1, t->cap.cuf, 0, 1, x));
-       }
+       CHECK(caps_move_absx(t, x));
       }
       break;
 
     case TTOF_XCUR | TTOF_YHOME:
+      /* Relative horizontal movement, with absolute vertical movement.
+       *
+       * The only thing to try is two separate motions.
+       */
+
       PUT1I(1, vpa, y);
       CHECK(caps_move_relative(t, x,
                               t->cap.cuf1, t->cap.cuf,
@@ -1586,21 +1919,39 @@ static int caps_move(struct tty *tty,
       break;
 
     default:
+      /* Anything else was an error. */
+
       rc = -1; goto end;
       break;
   }
 
+  /* If we turned off insert mode, we can turn it back on now. */
   if (!t->cap.mir && (t->tty.st.modes&TTMF_INS)) PUT0(0, smir);
 
+  /* And if we turned off any attributes, we can turn them back on. */
   if (!t->cap.msgr && a.f)
     CHECK(caps_setattr_internal(t, &a));
 
+  /* All done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_repeat@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int ch@ = character to write
+ *             @unsigned n@ = number of copies
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Write @n@ copies of the character @ch@ to the terminal.
+ */
+
 static int caps_repeat(struct tty *tty,
                       const struct gprintf_ops *gops, void *go,
                       int ch, unsigned n)
@@ -1610,7 +1961,10 @@ static int caps_repeat(struct tty *tty,
   unsigned wd, nn;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
+
+  /* If there isn't a capability for this then do it the stupid way. */
   if (!t->cap.rep)
     CHECK(stupid_repeat(tty, gops, go, ch, n));
   else {
@@ -1621,46 +1975,96 @@ static int caps_repeat(struct tty *tty,
       n -= nn;
     }
   }
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
-static int caps_erase(struct tty *tty,
-                     const struct gprintf_ops *gops, void *go,
+/* --- @caps_erase@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase portions of the current line or the whole display.
+ */
+
+static int caps_erase(struct tty *tty,
+                     const struct gprintf_ops *gops, void *go,
                      unsigned f)
 {
   struct tty_caps *t = (struct tty_caps *)tty;
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
-  if (f&TTEF_DSP)
+
+  if (!(f&TTEF_DSP)) {
+    /* Erase line.  This is easy. */
+
+    if (f&TTEF_BEGIN) PUT0(1, el1);
+    if (f&TTEF_END) PUT0(1, el);
+  } else {
+    /* Erase display.  Note that `terminfo' lacks a capability for `erase
+     * from start of display'.
+     */
+
     switch (f&(TTEF_BEGIN | TTEF_END)) {
       case 0:
+       /* Nothing to do. */
+
        break;
+
       case TTEF_BEGIN | TTEF_END:
+       /* Erase the whole display.  If we have a `clear', then use it;
+        * otherwise, synthesize it from `home' and `erase to end'.
+        */
+
        if (t->cap.clear) PUT0(t->tty.ht, clear);
        else { PUT0(1, home); PUT0(t->tty.ht, ed); }
        break;
+
       case TTEF_END:
+       /* Erase to the end of the display. */
+
        PUT0(t->tty.ht, ed);
        break;
+
       default:
+       /* Anything else is wrong. */
+
        rc = -1; goto end;
        break;
     }
-  else {
-    if (f&TTEF_BEGIN) PUT0(1, el1);
-    if (f&TTEF_END) PUT0(1, el);
   }
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_erch@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned n@ = number of characters to erase
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase a number of characters, starting from and including the
+ *             current cursor position.
+ */
+
 static int caps_erch(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     unsigned n)
@@ -1669,14 +2073,32 @@ static int caps_erch(struct tty *tty,
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
+
+  /* Produce the control sequence. */
   if (n) PUT1I(1, ech, n);
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_ins@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a number of blank characters or lines.
+ */
+
 static int caps_ins(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
                    unsigned f, unsigned n)
@@ -1685,17 +2107,34 @@ static int caps_ins(struct tty *tty,
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
+
+  /* Produce the control sequence. */
   if (f&TTIDF_LN)
     CHECK(caps_iterate(t, t->cap.il1, t->cap.il, CIF_PADMUL, 1, n));
   else
     CHECK(caps_iterate(t, t->cap.ich1, t->cap.ich, 0, 1, n));
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_inch@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int ch@ = character to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a single character.
+ */
+
 static int caps_inch(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     int ch)
@@ -1704,18 +2143,44 @@ static int caps_inch(struct tty *tty,
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
+
+  /* If the terminal has an insert mode, but it's not turned on, then this
+   * isn't going to work.
+   */
   if (t->cap.smir ? !(t->tty.st.modes&TTMF_INS) : !t->cap.ich)
     { rc = -1; goto end; }
+
+  /* If there's an insert-one-character sequence, then send that. */
   if (t->cap.ich) PUT0(1, ich);
+
+  /* Put the actual character. */
   CHECK(gops->putch(go, ch));
+
+  /* Some terminals need post-insertion padding, which turns up here. */
   if (t->cap.ip) PUT0(1, ip);
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
   return (rc);
 }
 
+/* --- @caps_del@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to delete
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Delete a number of characters or lines.
+ */
+
 static int caps_del(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
                    unsigned f, unsigned n)
@@ -1724,13 +2189,18 @@ static int caps_del(struct tty *tty,
   const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   int rc;
 
+  /* Prepare for output. */
   ops->cap.prepout(&t->tty, gops, go);
+
+  /* Send the control sequence. */
   if (n) {
     if (f&TTIDF_LN)
       CHECK(caps_iterate(t, t->cap.dl1, t->cap.dl, CIF_PADMUL, 1, n));
     else
       CHECK(caps_iterate(t, t->cap.dch1, t->cap.dch, 0, 1, n));
   }
+
+  /* Done. */
   rc = 0;
 end:
   if (ops->cap.flush(&t->tty)) rc = -1;
@@ -1744,6 +2214,7 @@ end:
 #undef PUT1I
 #undef PUT2I
 
+/* The operation functions for terminal backends based on `terminfo'. */
 #define TTY_CAPOPS                                                     \
   caps_setattr, caps_setmodes,                                         \
   caps_move, caps_repeat,                                              \
@@ -1755,12 +2226,43 @@ end:
 
 #ifdef HAVE_TERMCAP
 
+/* So `termcap' is pretty bad, actually.  It reads the terminal description
+ * from a file into a caller-provided buffer without knowing how big the
+ * buffer is.  The @tgetstr@ function partially decodes a string capability
+ * into another caller-provided buffer with no bounds checking.
+ *
+ * And then there's the way that pieces of `termcap' communicate with each
+ * other through global variables which the caller has to set for it.
+ *
+ * Finally, while output can be redirected under application control, output
+ * is strictly one-character-at-a-time, and there's no way to pass additional
+ * context information to the character-output function, so we need yet more
+ * global state.
+ *
+ * I'm not a fan.
+ */
+
+/* Additional state required by `termcap'. */
 struct tty_termcapslots {
-  char termbuf[4096], capbuf[4096], *capcur;
+  char capbuf[4096], *capcur;          /* string caps, and cursor */
 };
 struct tty_termcap { TTY_CAPSPFX; struct tty_termcapslots tc; };
 union tty_termcapu { struct tty_termcap tc; TTY_CAPSUSFX; };
 
+/* --- @termcap_boolcap@, @termcap_intcap@, @termcap_strcap@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @int uix, const char *info, const char *cap@ = names for the
+ *                     requested capability (as Unibilium index, and
+ *                     `terminfo' and `termcap' names
+ *
+ * Returns:    The requested capability value.  On failure,
+ *             @termcap_boolcap@ returns false, @termcap_intcap@ returns
+ *             %$-1$%, and @termcap_strcap@ returns a null pointer.
+ *
+ * Use:                Return a requested boolean, integer, or string capability.
+ */
+
 static int termcap_boolcap(struct tty *tty,
                           int uix, const char *info, const char *cap)
 {
@@ -1785,11 +2287,37 @@ static const char *termcap_strcap(struct tty *tty,
 {
   struct tty_termcap *t = (struct tty_termcap *)tty;
   const char *p;
+  size_t n;
 
-  p = tgetstr(cap, &t->tc.capcur); assert(p != (const char *)-1);
+  /* Try to detect overruns.  This is gnarly because C and `termcap'.  We're
+   * not allowed to compare out-of-bounds pointers.  The best I can manage is
+   * to assume that @capcur@ is maintained within @capbuf@, and check that
+   * the length of the new capability didn't push us over the limit.  Things
+   * are even harder because, e.g., `ncurses' doesn't actually make use of
+   * our @capbuf@ at all.
+   *
+   * (Of course, if it did, it's probably too late to expect anyhing good,
+   * but maybe we can apply the emergency brakes before things get too bad.)
+   */
+  n = t->tc.capcur - t->tc.capbuf;
+  p = tgetstr(cap, &t->tc.capcur);
+    assert(p != (const char *)-1);
+    assert(!p || n + strlen(p) < sizeof(t->tc.capbuf));
   return (p);
 }
 
+/* --- @termcap_put0@, @termcap_put1i@, @termcap_put2i@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected, for padding
+ *             @const char *cap@ = capability string to write, or null
+ *             @int i0, i1@ = integer parameters
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Format a capability string and send it to the terminal.
+ */
+
 static int termcap_put0(struct tty *tty,
                        unsigned npad, const char *cap)
 {
@@ -1811,26 +2339,74 @@ static int termcap_put2i(struct tty *tty,
   return (tputs(tgoto(cap, i1, i0), npad, caps_putch) == OK ? 0 : -1);
 }
 
+/* --- @termcap_cost@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected
+ *             @const char *cap@ = capability string to measure, or null
+ *             @int i0@ = argument
+ *
+ * Returns:    A linear cost for using the capability.  This is meaningless
+ *             if @cap@ is null.
+ */
+
+static size_t termcap_cost(struct tty *tty,
+                          unsigned npad, const char *cap, int i0)
+  { return (cap ? caps_cost(tty, npad, tgoto(cap, 0, i0)) : 0); }
+
+/* The `termcap' operations table. */
 static const union tty_capopsu termcap_ops = { {
   { caps_release, TTY_CAPOPS },
   { termcap_boolcap, termcap_intcap, termcap_strcap,
     caps_prepout, caps_flush,
-    termcap_put0, termcap_put1i, termcap_put2i }
+    termcap_put0, termcap_put1i, termcap_put2i,
+    termcap_cost }
 } };
 
+/* --- @termcap_init@ --- *
+ *
+ * Arguments:  @FILE *fp@ = the output stream
+ *
+ * Returns:    A pointer to a fresh terminal control block, or null on
+ *             failure.
+ *
+ * Use:                Initialize a terminal for use through `termcap'.
+ */
+
 static struct tty *termcap_init(FILE *fp)
 {
   union tty_termcapu *u = 0; struct tty *ret = 0;
+  char termbuf[4096];
   const char *term;
-
+  speed_t spd = B0;
+  char *t;
+
+  /* The `termcap' library makes use of global state, so we can only have one
+   * at a time.  Traditional `termcap' is actually less bad in this sense,
+   * but the `ncurses' emulation is tangled up with its `terminfo' machinery,
+   * which is backed by a large amount of global state.  So check that there
+   * aren't likely to be conflicts.
+   */
   if (caps_claim()) goto end;
+
+  /* Get the terminal name and try to find the terminal description. */
   term = getenv("TERM"); if (!term) goto end;
+  if (tgetent(termbuf, term) < 1) goto end;
+
+  /* Set up a fresh control block. */
   XNEW(u);
-  if (tgetent(u->tc.tc.termbuf, term) < 1) goto end;
   u->tc.tc.capcur = u->tc.tc.capbuf;
   u->tty.ops = &termcap_ops.tty;
-  common_init(&u->tty, fp);
+  common_init(&u->tty, fp, &spd);
   init_caps(&u->cap);
+
+  /* Set the `termcap' global variables. */
+  t = tgetstr("bc", &u->tc.tc.capcur); BC = t ? t : "\b";
+  UP = UNCONST(char, u->cap.cap.cuu1);
+  PC = u->cap.cap.pad ? *u->cap.cap.pad : 0;
+  ospeed = spd;
+
+  /* All done. */
   ret = &u->tty; u = 0;
 end:
   xfree(u); global_lock = ret; return (ret);
@@ -1842,6 +2418,33 @@ end:
 
 #ifdef HAVE_TERMINFO
 
+/* So `terminfo' is pretty bad too, actually.  The good news is that
+ * `termcap''s potential buffer overruns are gone.  The bad news is that
+ * `terminfo' goes all in on global state.  Rather than being put in a
+ * caller-provided buffer, the terminal description is read into global
+ * state.
+ *
+ * Annoyingly, `terminfo' adopts the same `tputs' machinery as `termcap', and
+ * therefore shares the one-character-at-a-time output and inability to pass
+ * contextual information through to the ouptut function.
+ *
+ * I'm not a fan of `terminfo' either.
+ */
+
+/* --- @terminfo_boolcap@, @terminfo_intcap@, @terminfo_strcap@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @int uix, const char *info, const char *cap@ = names for the
+ *                     requested capability (as Unibilium index, and
+ *                     `terminfo' and `termcap' names
+ *
+ * Returns:    The requested capability value.  On failure,
+ *             @termcap_boolcap@ returns false, @termcap_intcap@ returns
+ *             %$-1$%, and @termcap_strcap@ returns a null pointer.
+ *
+ * Use:                Return a requested boolean, integer, or string capability.
+ */
+
 static int terminfo_boolcap(struct tty *tty,
                            int uix, const char *info, const char *cap)
 {
@@ -1870,6 +2473,18 @@ static const char *terminfo_strcap(struct tty *tty,
   return (p);
 }
 
+/* --- @terminfo_put0@, @terminfo_put1i@, @terminfo_put2i@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected, for padding
+ *             @const char *cap@ = capability string to write, or null
+ *             @int i0, i1@ = integer parameters
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Format a capability string and send it to the terminal.
+ */
+
 static int terminfo_put0(struct tty *tty,
                         unsigned npad, const char *cap)
 {
@@ -1891,24 +2506,61 @@ static int terminfo_put2i(struct tty *tty,
   return (tputs(tparm(cap, i0, i1), npad, caps_putch) == OK ? 0 : -1);
 }
 
+/* --- @terminfo_cost@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected
+ *             @const char *cap@ = capability string to measure, or null
+ *             @int i0@ = argument
+ *
+ * Returns:    A linear cost for using the capability.  This is meaningless
+ *             if @cap@ is null.
+ */
+
+static size_t terminfo_cost(struct tty *tty,
+                           unsigned npad, const char *cap, int i0)
+  { return (cap ? caps_cost(tty, npad, tparm(cap, i0)) : 0); }
+
+/* The `terminfo' operations table. */
 static const union tty_capopsu terminfo_ops = { {
   { caps_release, TTY_CAPOPS },
   { terminfo_boolcap, terminfo_intcap, terminfo_strcap,
     caps_prepout, caps_flush,
-    terminfo_put0, terminfo_put1i, terminfo_put2i }
+    terminfo_put0, terminfo_put1i, terminfo_put2i,
+    terminfo_cost }
 } };
 
+/* --- @terminfo_init@ --- *
+ *
+ * Arguments:  @FILE *fp@ = the output stream
+ *
+ * Returns:    A pointer to a fresh terminal control block, or null on
+ *             failure.
+ *
+ * Use:                Initialize a terminal for use through `terminfo'.
+ */
+
 static struct tty *terminfo_init(FILE *fp)
 {
   union tty_capsu *u = 0; struct tty *ret = 0;
   int err;
 
+  /* The `termcap' library makes extensive use of global state, so we can
+   * only have one at a time.  Check that there aren't likely to be
+   * conflicts.
+   */
   if (caps_claim()) goto end;
+
+  /* Get the terminal description. */
   if (setupterm(0, fp ? fileno(fp) : -1, &err) != OK || err < 1) goto end;
+
+  /* Set up a fresh control block. */
   XNEW(u);
   u->tty.ops = &terminfo_ops.tty;
-  common_init(&u->tty, fp);
+  common_init(&u->tty, fp, 0);
   init_caps(&u->cap);
+
+  /* All done. */
   ret = &u->tty; u = 0;
 end:
   xfree(u); global_lock = ret; return (ret);
@@ -1920,16 +2572,66 @@ end:
 
 #ifdef HAVE_UNIBILIUM
 
+/* Unibilium is a `terminfo' library for the 21st century.  It avoids all of
+ * the mistakes of the `termcap' and `terminfo' interfaces.  Honestly, it's a
+ * bit too `modern' for my tastes: the type bureaucracy involved with
+ * `unibi_var_t' seems unnecessarily heavyweight (but won't actually bother
+ * us much here).
+ *
+ * It's not all sunshine and roses.  The library makes a principled decision
+ * to avoid actually messing with terminals directly at all, which seems
+ * fair enough -- even laudable, maybe.  But there's also no help provided to
+ * generate padding characters, so we have to do that by ourselves, which
+ * ends up being perhaps the most complicated part of this whole business.
+ *
+ * Also, while the caller-provided output functions can at least be passed a
+ * context pointer, they can't report errors, which means that we must leave
+ * a pending error indication.
+ *
+ * Oh, and the name is actually really difficult to type.
+ */
+
+/* Additional state required for Unibilium. */
 struct tty_unibislots {
-  unibi_term *ut;
-  unibi_var_t dy[26], st[26];
-  const struct gprintf_ops *gops; void *go;
-  char buf[4096]; size_t n;
-  int err;
+  unibi_term *ut;                      /* the terminal description */
+  unibi_var_t dy[26], st[26];          /* variables used by cap strings */
+  const struct gprintf_ops *gops; void *go; /* output destination */
+  char buf[BUFSZ]; size_t n;           /* output buffer and length */
+  int err;                             /* pending error indication */
 };
 struct tty_unibilium { TTY_CAPSPFX; struct tty_unibislots u; };
 union tty_unibiliumu { struct tty_unibilium u; TTY_CAPSUSFX; };
 
+/* --- @termunibi_release@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Release resources held by the control block.
+ */
+
+static void termunibi_release(struct tty *tty)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  unibi_destroy(t->u.ut);
+}
+
+/* --- @termcap_boolcap@, @termcap_intcap@, @termcap_strcap@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @int uix, const char *info, const char *cap@ = names for the
+ *                     requested capability (as Unibilium index, and
+ *                     `terminfo' and `termcap' names
+ *
+ * Returns:    The requested capability value.  On failure,
+ *             @termcap_boolcap@ returns false, @termcap_intcap@ returns
+ *             %$-1$%, and @termcap_strcap@ returns a null pointer.
+ *
+ * Use:                Return a requested boolean, integer, or string capability.
+ */
+
 static int termunibi_boolcap(struct tty *tty,
                            int uix, const char *info, const char *cap)
 {
@@ -1955,86 +2657,185 @@ static const char *termunibi_strcap(struct tty *tty,
   return (unibi_get_str(t->u.ut, uix));
 }
 
+/* --- @termunibi_prepout@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer (ignored)
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *
+ * Returns:    ---
+ *
+ * Use:                Prepare output to the given destination.  Check that the
+ *             output buffer is initially empty (i.e., that it was properly
+ *             flushed last time), and make a note of the output
+ *             destination.
+ */
+
+static void termunibi_prepout(struct tty *tty,
+                             const struct gprintf_ops *gops, void *go)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  assert(!t->u.n); t->u.gops = gops; t->u.go = go;
+}
+
+/* Context for output operations. */
 struct termunibi_outctx {
-  struct tty_unibilium *t;
+  struct tty_unibilium *t;             /* control block pointer */
+  unsigned npad;                       /* number of lines, for padding */
 };
 
+/* --- @termunibi_putm@ --- *
+ *
+ * Arguments:  @void *ctx@ = output context pointer
+ *             @const char *p, size_t sz@ = buffer to write
+ *
+ * Returns:    ---
+ *
+ * Use:                Send the material in the buffer to the current output
+ *             destination.
+ */
+
 static void termunibi_putm(void *ctx, const char *p, size_t sz)
 {
-  struct tty_unibilium *t = ctx;
+  struct termunibi_outctx *out = ctx;
+  struct tty_unibilium *t = out->t;
   size_t n;
 
-  n = sizeof(t->u.buf) - t->u.n;
-  if (sz <= n)
-    { memcpy(t->u.buf + t->u.n, p, sz); t->u.n += sz; }
-  else {
-    if (n) { memcpy(t->u.buf + t->u.n, p, n); p += n; sz -= n; }
-    for (;;) {
-      if (t->u.gops->putm(t->u.go, t->u.buf, sizeof(t->u.buf)))
-       t->u.err = -1;
-      if (sz <= sizeof(t->u.buf)) break;
-      memcpy(t->u.buf, p, sizeof(t->u.buf));
-       p += sizeof(t->u.buf); sz -= sizeof(t->u.buf);
-    }
+  /* Find out how much space is left in the buffer.  As for the common
+   * `termcap'/`terminfo' machinery above, by policy, we leave the buffer
+   * with at least one byte of space.
+   */
+  n = BUFSZ - t->u.n; assert(n > 0);
+
+  /* Save the output into the buffer. */
+  if (sz < n) {
+    /* There's enough space in the buffer for everything.  Just store all of
+     * it.
+     */
+
+    memcpy(t->u.buf + t->u.n, p, sz); t->u.n += sz;
+  } else if (sz - n < THRESH) {
+    /* We don't really have enough left over to be worth cycling the output
+     * machinery again yet.
+     */
+
+    memcpy(t->u.buf + t->u.n, p, n); p += n; sz -= n;
+    if (t->u.gops->putm(t->u.go, t->u.buf, BUFSZ)) t->u.err = -1;
     memcpy(t->u.buf, p, sz); t->u.n = sz;
+  } else {
+    /* There's enough that it's worth avoiding the copy.  Flush whatever's in
+     * the buffer, and inhale the entire new input in one go, leaving the
+     * buffer empty.
+     */
+
+    if (t->u.gops->putm(t->u.go, t->u.buf, t->u.n)) t->u.err = -1;
+      t->u.n = 0;
+    if (t->u.gops->putm(t->u.go, p, sz)) t->u.err = -1;
   }
 }
 
-static void termunibi_pad(void *ctx, size_t ms, int mulp, int forcep)
+/* --- @termunibi_pad@ --- *
+ *
+ * Arguments:  @void *ctx@ = control block pointer
+ *             @size_t delay@ = tenths of milliseconds to pad
+ *             @int mulp@ = nonzero if padding is per line
+ *             @int forcep@ = nonzero if padding is unconditional
+ *                     (regardless of, e.g., baud rate or flow control)
+ *
+ * Returns:    ---
+ *
+ * Use:                Pad for a given duration.
+ */
+
+static void termunibi_pad(void *ctx, size_t delay, int mulp, int forcep)
 {
-  struct tty_unibilium *t = ctx;
+  struct termunibi_outctx *out = ctx;
+  struct tty_unibilium *t = out->t;
   struct timeval tv;
+  unsigned long d;
   int pc;
   size_t sz, n;
 
-  /* Based on 7 data bits, 1 stop bit, 1 parity bit. */
-#define BITS_PER_KB 9000
+  /* Determine the actual delay. */
+  if (mulp) d = (unsigned long)delay*out->npad;
+  else d = delay;
 
+  /* There's nothing to do unless (a) we're forced, or (b) the baud rate is
+   * high enough and flow control isn't advertised.
+   */
   if (forcep || (t->tty.baud >= t->cap.pb && !t->cap.xon)) {
+
     if (t->cap.npc) {
-      tv.tv_sec = ms/1000; tv.tv_usec = 1000*(ms%1000);
+      /* There's no safe pad character to send, so we'll just have to wait
+       * for long enough.
+       *
+       * If output is buffered downstream of us then this isn't going to work
+       * correctly.  Honestly, that's a problem for the other output drivers
+       * too, and it only comes into clear focus here.
+       */
+
+      /* Flush the output. */
       if (t->u.n) {
-       if (t->u.gops->putm(t->u.go, t->u.buf, sizeof(t->u.buf)))
-         t->u.err = -1;
+       if (t->u.gops->putm(t->u.go, t->u.buf, BUFSZ)) t->u.err = -1;
        t->u.n = 0;
       }
       if (t->tty.fpout) fflush(t->tty.fpout);
+
+      /* Do nothing for a little while. */
+      tv.tv_sec = d/10000; tv.tv_usec = 100*(d%10000);
       select(0, 0, 0, 0, &tv);
     } else {
+      /* There's a pad character, so figure out how many we need and send
+       * them.
+       */
+
+      /* Determine the number of pad character and the pad value. */
+      sz = caps_padchars(&t->tty, d, forcep ? CPF_FORCE : 0);
       pc = t->cap.pad ? *t->cap.pad : 0;
-      sz = (ms*t->tty.baud + BITS_PER_KB - 1)/BITS_PER_KB;
-      n = sizeof(t->u.buf) - t->u.n;
-      if (sz <= n)
-       { memset(t->u.buf + t->u.n, pc, sz); t->u.n += sz; }
-      else {
-       if (n) { memset(t->u.buf + t->u.n, pc, sz); sz -= n; }
-       if (t->u.gops->putm(t->u.go, t->u.buf, sizeof(t->u.buf)))
-         t->u.err = -1;
-       if (sz < sizeof(t->u.buf))
-         memset(t->u.buf, pc, sz);
-       else {
-         memset(t->u.buf, pc, sizeof(t->u.buf));
-         do {
-           if (t->u.gops->putm(t->u.go, t->u.buf, sizeof(t->u.buf)))
-             t->u.err = -1;
-           sz -= sizeof(t->u.buf);
-         } while (sz > sizeof(t->u.buf));
+
+      /* Work out how much buffer space is available.  Again, by policy, the
+       * buffer is always left with at least one byte of capacity.
+       */
+      n = BUFSZ - t->u.n; assert(n);
+      if (sz < n) {
+       /* The padding will fit in the buffer. */
+
+       memset(t->u.buf + t->u.n, pc, sz); t->u.n += sz;
+      } else if (sz - n < BUFSZ) {
+       /* There's not enough to fill the buffer a second time.  Do this in
+        * two pieces.
+        */
+
+       memset(t->u.buf + t->u.n, pc, n); sz -= n;
+       if (t->u.gops->putm(t->u.go, t->u.buf, BUFSZ)) t->u.err = -1;
+       memset(t->u.buf, pc, sz); t->u.n = sz;
+      } else {
+       /* We'll fill the whole buffer at least once.  Flush what we have
+        * immediately and then do whole buffer-full sends.
+        */
+
+       if (t->u.gops->putm(t->u.go, t->u.buf, t->u.n)) t->u.err = -1;
+       memset(t->u.buf, pc, BUFSZ);
+       while (sz >= BUFSZ) {
+         if (t->u.gops->putm(t->u.go, t->u.buf, BUFSZ)) t->u.err = -1;
+         sz -= BUFSZ;
        }
        t->u.n = sz;
       }
     }
   }
-
-#undef BITS_PER_KB
 }
 
-static void termunibi_prepout(struct tty *tty,
-                             const struct gprintf_ops *gops, void *go)
-{
-  struct tty_unibilium *t = (struct tty_unibilium *)tty;
-
-  assert(!t->u.n); t->u.gops = gops; t->u.go = go;
-}
+/* --- @termunibi_flush@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer (ignored)
+ *
+ * Returns:    Zero for success, %$-1$% if error pending.
+ *
+ * Use:                Flush the output buffer to the backend.  If an error is
+ *             pending, clear it and return failure.
+ */
 
 static int termunibi_flush(struct tty *tty)
 {
@@ -2048,16 +2849,30 @@ static int termunibi_flush(struct tty *tty)
   t->u.err = 0; return (rc);
 }
 
+/* --- @termunibi_put0@, @termunibi_put1i@, @termunibi_put2i@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected, for padding
+ *             @const char *cap@ = capability string to write, or null
+ *             @int i0, i1@ = integer parameters
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Format a capability string and send it to the terminal.
+ */
+
 static int termunibi_put0(struct tty *tty,
                          unsigned npad, const char *cap)
 {
   struct tty_unibilium *t = (struct tty_unibilium *)tty;
+  struct termunibi_outctx out;
   unibi_var_t arg[9];
 
   if (!cap) return (-1);
+  out.t = t; out.npad = npad;
   unibi_format(t->u.dy, t->u.st, cap, arg,
-              termunibi_putm, t,
-              termunibi_pad, t);
+              termunibi_putm, &out,
+              termunibi_pad, &out);
   return (0);
 }
 
@@ -2065,13 +2880,15 @@ static int termunibi_put1i(struct tty *tty,
                           unsigned npad, const char *cap, int i0)
 {
   struct tty_unibilium *t = (struct tty_unibilium *)tty;
+  struct termunibi_outctx out;
   unibi_var_t arg[9];
 
   if (!cap) return (-1);
   arg[0] = unibi_var_from_num(i0);
+  out.t = t; out.npad = npad;
   unibi_format(t->u.dy, t->u.st, cap, arg,
-              termunibi_putm, t,
-              termunibi_pad, t);
+              termunibi_putm, &out,
+              termunibi_pad, &out);
   return (0);
 }
 
@@ -2080,45 +2897,125 @@ static int termunibi_put2i(struct tty *tty,
                           const char *cap, int i0, int i1)
 {
   struct tty_unibilium *t = (struct tty_unibilium *)tty;
+  struct termunibi_outctx out;
   unibi_var_t arg[9];
 
   if (!cap) return (-1);
   arg[0] = unibi_var_from_num(i0);
   arg[1] = unibi_var_from_num(i1);
+  out.t = t; out.npad = npad;
   unibi_format(t->u.dy, t->u.st, cap, arg,
-              termunibi_putm, t,
-              termunibi_pad, t);
+              termunibi_putm, &out,
+              termunibi_pad, &out);
   return (0);
 }
 
-static void termunibi_release(struct tty *tty)
+/* Context for cost counting. */
+struct termunibi_costctx {
+  struct tty_unibilium *t;             /* terminal control block */
+  unsigned npad;                       /* number of lines affected */
+  size_t n;                            /* accumulated cost, in chars */
+};
+
+/* --- @termunibi_costputm@ --- *
+ *
+ * Arguments:  @void *ctx@ = output context pointer
+ *             @const char *p, size_t sz@ = buffer to write
+ *
+ * Returns:    ---
+ *
+ * Use:                Count the number of characters written.
+ */
+
+static void termunibi_costputm(void *ctx, const char *p, size_t sz)
+  { struct termunibi_costctx *c = ctx; c->n += sz; }
+
+/* --- @termunibi_costpad@ --- *
+ *
+ * Arguments:  @void *ctx@ = control block pointer
+ *             @size_t delay@ = tenths of milliseconds to pad
+ *             @int mulp@ = nonzero if padding is per line
+ *             @int forcep@ = nonzero if padding is unconditional
+ *                     (regardless of, e.g., baud rate or flow control)
+ *
+ * Returns:    ---
+ *
+ * Use:                Count the character equivalent of the requested delay.
+ */
+
+static void termunibi_costpad(void *ctx, size_t delay, int mulp, int forcep)
+{
+  struct termunibi_costctx *c = ctx;
+
+  if (mulp) delay *= c->npad;
+  c->n += caps_padchars(&c->t->tty, delay, CPF_FORCE);
+}
+
+/* --- @termunibi_cost@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @unsigned npad@ = number of lines affected
+ *             @const char *cap@ = capability string to measure, or null
+ *             @int i0@ = argument
+ *
+ * Returns:    A linear cost for using the capability.  This is meaningless
+ *             if @cap@ is null.
+ */
+
+static size_t termunibi_cost(struct tty *tty,
+                             unsigned npad, const char *cap, int i0)
 {
   struct tty_unibilium *t = (struct tty_unibilium *)tty;
+  struct termunibi_costctx c;
+  unibi_var_t arg[9];
 
-  unibi_destroy(t->u.ut);
+  if (!cap) return (0);
+  arg[0] = unibi_var_from_num(i0);
+  c.t = t; c.npad = npad; c.n = 0;
+  unibi_format(t->u.dy, t->u.st, cap, arg,
+              termunibi_costputm, &c,
+              termunibi_costpad, &c);
+  return (c.n);
 }
 
+/* The `terminfo' operations table. */
 static const union tty_capopsu termunibi_ops = { {
   { termunibi_release, TTY_CAPOPS },
   { termunibi_boolcap, termunibi_intcap, termunibi_strcap,
     termunibi_prepout, termunibi_flush,
-    termunibi_put0, termunibi_put1i, termunibi_put2i }
+    termunibi_put0, termunibi_put1i, termunibi_put2i,
+    termunibi_cost }
 } };
 
+/* --- @termunibi_init@ --- *
+ *
+ * Arguments:  @FILE *fp@ = the output stream
+ *
+ * Returns:    A pointer to a fresh terminal control block, or null on
+ *             failure.
+ *
+ * Use:                Initialize a terminal for use through Unibilium.
+ */
+
 static struct tty *termunibi_init(FILE *fp)
 {
   union tty_unibiliumu *u = 0; struct tty *ret = 0;
   unibi_term *ut = 0;
-  const char *term;
 
-  term = getenv("TERM"); if (!term) goto end;
-  ut = unibi_from_term(term); if (!ut) goto end;
+  /* Collect the terminal description. */
+  ut = unibi_from_env(); if (!ut) goto end;
+
+  /* Set up a fresh control block. */
   XNEW(u);
   u->tty.ops = &termunibi_ops.tty;
   u->u.u.ut = ut; ut = 0;
-  u->u.u.n = 0; u->u.u.err = 0;
-  common_init(&u->tty, fp);
+  common_init(&u->tty, fp, 0);
   init_caps(&u->cap);
+
+  /* Initialize the buffer state. */
+  u->u.u.n = 0; u->u.u.err = 0;
+
+  /* All done. */
   ret = &u->tty; u = 0;
 end:
   xfree(u); if (ut) unibi_destroy(ut);
@@ -2129,16 +3026,10 @@ end:
 
 /*----- ANSI terminals ----------------------------------------------------*/
 
-struct tty_ansislots {
-  unsigned f;
-#define TAF_CNCATTR 1u                 /*   attributes can be cancelled */
-#define TAF_EDITN 2u                   /*   insert/delete multiple */
-#define TAF_SEMI 4u                    /*   semicolons in CSI 38 m colour */
-};
-struct tty_ansi { TTY_BASEPFX; struct tty_ansislots ansi; };
-union tty_ansiu { struct tty_ansi ansi; TTY_BASEUSFX; };
-
-/* Control sequences.
+/* Here we dispense with external libraries and take a wild guess that the
+ * terminal accepts some variant of ANSI control sequences.
+ *
+ * Here's a quick reference of the control sequences that we use here.
  *
  *   * CUP: \33 [ Y ; X H              `cursor position' [vt100]
  *
@@ -2204,15 +3095,59 @@ union tty_ansiu { struct tty_ansi ansi; TTY_BASEUSFX; };
  *      P = 23, X = 0                  restore title and icon [xterm]
  */
 
+/* Additional state required for ANSI terminals. */
+struct tty_ansislots {
+  unsigned f;
+#define TAF_CNCATTR 1u                 /*   attributes can be cancelled */
+#define TAF_EDITN 2u                   /*   insert/delete multiple */
+#define TAF_SEMI 4u                    /*   semicolons in CSI 38 m colour */
+};
+struct tty_ansi { TTY_BASEPFX; struct tty_ansislots ansi; };
+union tty_ansiu { struct tty_ansi ansi; TTY_BASEUSFX; };
+
+/* --- @ansi_release@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Release resources held by the control block.  (There's
+ *             actually nothing to do here.)
+ */
+
 static void ansi_release(struct tty *tty) { ; }
 
+/* Some simple macros we use throughout. */
 #define PUTCH(ch) CHECK(gops->putch(go, (ch)))
 #define PUTLIT(lit) CHECK(gops->putm(go, (lit), sizeof(lit) - 1))
+
+/* Shared macro for @ansi_setcolour@ and @ansi_setattr@.  The output has the
+ * form `ESC [ ... ; ... ; ... m', so there's a semicolon before each
+ * parameter other than the first.  The @TAF_SEMI@ flag here keeps track of
+ * whether a semicolon is wanted before the next item, and this macro is
+ * responsible for writing it when necessary.
+ */
 #define SEMI do {                                                      \
   if (!(f&TAF_SEMI)) f |= TAF_SEMI;                                    \
   else PUTCH(';');                                                     \
 } while (0)
 
+/* --- @ansi_setcolour@ --- *
+ *
+ * Arguments:  @struct tty_caps *t@ = extended control block pointer
+ *             @unsigned *f_inout@ = flags (updated)
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int norm, br@ = normal and bright base codes -- 30 and 90
+ *                     for foreground, or 40 and 100 for background
+ *             @uint32 spc, clr@ = the colour space and number
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Emit the correct control string to set the foreground or
+ *             background colour as indicated.
+ */
+
 static int ansi_setcolour(struct tty_ansi *t, unsigned *f_inout,
                          const struct gprintf_ops *gops, void *go,
                          int norm, int br,
@@ -2270,19 +3205,50 @@ end:
   *f_inout = f; return (rc);
 }
 
+/* --- @ansi_setattr@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @const struct tty_attr *a@ = attribute to set, already
+ *                     clamped
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Arrange to display future characters with the display
+ *             attributes indicated by @a@.
+ */
+
 static int ansi_setattr(struct tty *tty,
                        const struct gprintf_ops *gops, void *go,
                        const struct tty_attr *a)
 {
   struct tty_ansi *t = (struct tty_ansi *)tty;
-  uint32 diff;
+  uint32 diff, m;
   int rc = 0;
   unsigned z, c, f = 0;
 
+  /* Work out what, if anything, needs doing.  Since we start by writing
+   * `ESC [' unconditionally below, if there's actually no work to do at all,
+   * we need to stop early.
+   */
   diff = a->f ^ t->tty.st.attr.f;
   if (!diff && a->fg == t->tty.st.attr.fg && a->bg == t->tty.st.attr.bg)
-    return (0);
+    { rc = 0; goto end; }
+
+  /* We're committed.  Write the start of the `SGR' sequence. */
+  PUTLIT("\33[");
+
+  /* Form a basic strategy.
+   *
+   * As for @caps_setattr@ above, we need to decide betwen cancelling
+   * individual attributes or resetting the whole lot and reinstating the
+   * attributes which were cancelled unnecessarily.
+   */
 
+  /* Start by adding up the costs of cancelling individual attributes.
+   * Early terminals can't do this, but we'll deal with that problem later.
+   */
   c = 0;
 #define CLEARP(mask) ((diff&(mask)) && !(a->f&(mask)))
   if (CLEARP(TTAF_LNMASK)) c += 3;
@@ -2294,31 +3260,44 @@ static int ansi_setattr(struct tty *tty,
   if (CLEARP(TTAF_BGSPCMASK)) c += 3;
 #undef CLEARP
 
+  /* Count up the cost of cancelling everything and then putting back the
+   * ones that we actually wanted.
+   */
   z = 0;
-  switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
-    case TTLN_ULINE: z += 2; break;
-    case TTLN_UULINE: z += 3; break;
-  }
-  if (a->f&TTAF_WTMASK) z += 2;
-  if (a->f&TTAF_INVV) z += 2;
-  if (a->f&TTAF_STRIKE) z += 2;
-  if (a->f&TTAF_ITAL) z += 2;
-#define COLOURCOST(col) do {                                           \
-  switch ((a->f&TTAF_##col##SPCMASK) >> TTAF_##col##SPCSHIFT) {                \
-    case TTCSPC_1BPC: case TTCSPC_1BPCBR: z += 3; break;               \
-    case TTCSPC_4LPC: case TTCSPC_8LGS: z += 8; break;                 \
-    case TTCSPC_6LPC: case TTCSPC_24LGS: z += 9; break;                        \
-    case TTCSPC_8BPC: z += 16; break;                                  \
-  }                                                                    \
+  if (!(diff&TTAF_LNMASK))
+    switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+      case TTLN_ULINE: z += 2; break;
+      case TTLN_UULINE: z += 3; break;
+    }
+  if (!(diff&TTAF_WTMASK))
+    switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+      case TTWT_BOLD: z += 2; break;
+      case TTWT_DIM: z += 2; break;
+    }
+  m = a->f&~diff;
+  if (m&TTAF_INVV) z += 2;
+  if (m&TTAF_STRIKE) z += 2;
+  if (m&TTAF_ITAL) z += 2;
+#define COLOURCOST(G, g) do {                                          \
+  if (!(diff&TTAF_##G##SPCMASK) &&                                     \
+      (a->f&TTAF_##G##SPCMASK) && a->g == t->tty.st.attr.g)            \
+    switch ((a->f&TTAF_##G##SPCMASK) >> TTAF_##G##SPCSHIFT) {          \
+      case TTCSPC_1BPC: case TTCSPC_1BPCBR: z += 3; break;             \
+      case TTCSPC_4LPC: case TTCSPC_8LGS: z += 8; break;               \
+      case TTCSPC_6LPC: case TTCSPC_24LGS: z += 9; break;              \
+      case TTCSPC_8BPC: z += 16; break;                                        \
+    }                                                                  \
 } while (0)
-  COLOURCOST(FG); COLOURCOST(BG);
+  COLOURCOST(FG, fg); COLOURCOST(BG, bg);
 #undef COLOURCOST
 
-  PUTLIT("\33[");
-
+  /* If cancelling everything is cheaper, or we can't cancel individual
+   * attributes after all, then reset everything.
+   */
   if (z < c || (c && !(t->ansi.f&TAF_CNCATTR)))
     { SEMI; diff = a->f; t->tty.st.attr.fg = t->tty.st.attr.bg = 0; }
 
+  /* Line style. */
   if (diff&TTAF_LNMASK)
     switch ((a->f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
       case TTLN_NONE: SEMI; PUTLIT("24"); break;
@@ -2327,6 +3306,7 @@ static int ansi_setattr(struct tty *tty,
       default: rc = -1; goto end;
     }
 
+  /* Text weight. */
   if (diff&TTAF_WTMASK)
     switch ((a->f&TTAF_WTMASK) >> TTAF_WTSHIFT) {
       case TTWT_MED: SEMI; PUTLIT("22"); break;
@@ -2335,6 +3315,7 @@ static int ansi_setattr(struct tty *tty,
       default: rc = -1; goto end;
     }
 
+  /* Other text effects. */
   if (diff&TTAF_INVV)
     { SEMI; if (a->f&TTAF_INVV) PUTCH('7'); else PUTLIT("27"); }
   if (diff&TTAF_STRIKE)
@@ -2342,6 +3323,7 @@ static int ansi_setattr(struct tty *tty,
   if (diff&TTAF_ITAL)
     { SEMI; if (a->f&TTAF_ITAL) PUTCH('3'); else PUTLIT("23"); }
 
+  /* Colours. */
   if (diff&TTAF_FGSPCMASK || a->fg != tty->st.attr.fg)
     CHECK(ansi_setcolour(t, &f, gops, go, 30, 90,
                         (a->f&TTAF_FGSPCMASK) >> TTAF_FGSPCSHIFT, a->fg));
@@ -2349,12 +3331,25 @@ static int ansi_setattr(struct tty *tty,
     CHECK(ansi_setcolour(t, &f, gops, go, 40, 100,
                         (a->f&TTAF_BGSPCMASK) >> TTAF_BGSPCSHIFT, a->bg));
 
+  /* All done. */
   PUTCH('m'); rc = 0;
 end:
   t->tty.st.attr = *a; return (rc);
-
 }
 
+/* --- @ansi_setmodes@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @uint32 modes_bic, modes_xor@ = masks to apply to the modes
+ *                     settings
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Set the requested terminal modes.
+ */
+
 static int ansi_setmodes(struct tty *tty,
                         const struct gprintf_ops *gops, void *go,
                         uint32 modes_bic, uint32 modes_xor)
@@ -2366,39 +3361,66 @@ static int ansi_setmodes(struct tty *tty,
   modes = (tty->st.modes&~modes_bic) ^ modes_xor;
   diff = modes ^ tty->st.modes;
 
+  /* Auto-margins. */
   if (diff&TTMF_AUTOM) {
     if (modes&TTMF_AUTOM) PUTLIT("\33[?7h");
     else PUTLIT("\33[?7l");
   }
 
+  /* Fullscreen. */
   if (diff&TTMF_FSCRN) {
     if (modes&TTMF_FSCRN) PUTLIT("\33[?1049h\33[22;0;0t");
     else PUTLIT("\33[?1049l\33[23;0;0t");
   }
 
+  /* Visible cursor. */
   if (diff&TTMF_CVIS) {
     if (modes&TTMF_CVIS) PUTLIT("\33[?25h");
     else PUTLIT("\33[?25l");
   }
 
+  /* Insert. */
   if (diff&TTMF_INS) {
     if (modes&TTMF_INS) PUTLIT("\33[4h");
     else PUTLIT("\33[4l");
   }
 
+  /* Done. */
   rc = 0;
 end:
   tty->st.modes = modes;
   return (rc);
 }
 
+/* --- @ansi_move@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned orig@ = origin
+ *             @int y, x@ = new cursor position
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Move the cursor.
+ */
+
 static int ansi_move(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     unsigned orig, int y, int x)
 {
+  static const char bs[4] = { '\b', '\b', '\b', '\b' };
   int rc;
 
+  /* Check that the arguments are basically sensible. */
+  if ((!(orig&TTOF_YCUR) && y < 0) || (!(orig&TTOF_XCUR) && x < 0))
+    { rc = -1; goto end; }
+
   if (orig == TTORG_HOME) {
+    /* Fully absolute positioning.  We can omit the arguments for the topmost
+     * row and/or leftmost column.
+     */
+
     if (!x) {
       if (!y) PUTLIT("\33[H");
       else CHECK(gprintf(gops, go, "\33[%dH", y + 1));
@@ -2406,15 +3428,29 @@ static int ansi_move(struct tty *tty,
       if (!y) CHECK(gprintf(gops, go, "\33[;%dH", x + 1));
       else CHECK(gprintf(gops, go, "\33[%d,%dH", y + 1, x + 1));
     }
-  } else if (orig == (TTOF_XHOME | TTOF_YCUR) && x == 0 && y == 1)
+  } else if (orig == (TTOF_XHOME | TTOF_YCUR) && x == 0 && y == 1) {
+    /* Special case for starting a new line. */
+
     PUTLIT("\r\n");
-  else {
+  } else {
+    /* More complex motion.  Handle the horizontal and vertical motions
+     * separately.
+     */
+
+    /* First, vertical motion.  This is simpler. */
     if (!(orig&TTOF_YCUR)) CHECK(gprintf(gops, go, "\33[%dd", y + 1));
     else if (y == -1) PUTLIT("\33[A");
     else if (y < 0) CHECK(gprintf(gops, go, "\33[%dA", -y));
     else if (y == +1) PUTLIT("\33[B"); /* not %|^J|%! */
     else if (y > 1) CHECK(gprintf(gops, go, "\33[%dB", y));
+
+    /* Next, horizontal motion. */
     if (!(orig&TTOF_XCUR)) {
+      /* Absolute positioning. Use a carriage return if (a) we want the
+       * leftmost column anyway, or (b) the terminal can't handle absolute
+       * horizontal positioning.
+       */
+
       if (!x)
        PUTCH('\r');
       else if (tty->ocaps&TTCF_MIXMV)
@@ -2422,17 +3458,37 @@ static int ansi_move(struct tty *tty,
       else
        CHECK(gprintf(gops, go, "\r\33[%dC", x));
     } else {
+      /* Relative positioning.  We can go back one space simply using the
+       * backspace control sequence.  The `CUB' sequence is at least four
+       * characters long.
+       */
+
       if (x == -1) PUTCH('\b');
+      else if (-4 <= x && x < -1) CHECK(gops->putm(go, bs, -x));
       else if (x < 0) CHECK(gprintf(gops, go, "\33[%dD", -x));
       else if (x == +1) PUTLIT("\33[C");
       else if (x > 0) CHECK(gprintf(gops, go, "\33[%dC", x));
     }
   }
+
+  /* Done. */
   rc = 0;
 end:
   return (rc);
 }
 
+/* --- @ansi_erase@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase portions of the current line or the whole display.
+ */
+
 static int ansi_erase(struct tty *tty,
                      const struct gprintf_ops *gops, void *go,
                      unsigned f)
@@ -2458,6 +3514,19 @@ end:
   return (rc);
 }
 
+/* --- @ansi_erch@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned n@ = number of characters to erase
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase a number of characters, starting from and including the
+ *             current cursor position.
+ */
+
 static int ansi_erch(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     unsigned n)
@@ -2471,6 +3540,19 @@ end:
   return (rc);
 }
 
+/* --- @caps_ins@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a number of blank characters or lines.
+ */
+
 static int ansi_ins(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
                    unsigned f, unsigned n)
@@ -2489,14 +3571,40 @@ end:
   return (rc);
 }
 
+/* --- @caps_inch@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int ch@ = character to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a single character.
+ */
+
 static int ansi_inch(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
                     int ch)
 {
+  /* There's actually nothing to do here except write the character. */
   if (!(tty->st.modes&TTMF_INS)) return (-1);
   else return (gops->putch(go, ch));
 }
 
+/* --- @caps_del@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to delete
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Delete a number of characters or lines.
+ */
+
 static int ansi_del(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
                    unsigned f, unsigned n)
@@ -2521,6 +3629,7 @@ end:
 
 #undef CHECK
 
+/* The ANSI operations table. */
 static const struct tty_ops ansi_ops = {
   ansi_release,
   ansi_setattr, ansi_setmodes,
@@ -2528,8 +3637,19 @@ static const struct tty_ops ansi_ops = {
   ansi_erase, ansi_erch, ansi_ins, ansi_inch, ansi_del
 };
 
+/* --- @termcap_init@ --- *
+ *
+ * Arguments:  @FILE *fp@ = the output stream
+ *
+ * Returns:    A pointer to a fresh terminal control block, or null on
+ *             failure.
+ *
+ * Use:                Initialize a terminal for use with ANSI control sequences.
+ */
+
 static struct tty *ansi_init(FILE *fp)
 {
+  /* Useful collections of capabilities. */
 #define COLS_NO 0
 #define COLS_8 (TTACF_FG | TTACF_BG | TTACF_1BPC)
 #define COLS_16 (COLS_8 | TTACF_1BPCBR)
@@ -2541,6 +3661,7 @@ static struct tty *ansi_init(FILE *fp)
                  TTCF_DELCH | TTCF_DELLN |                             \
                  TTCF_INSCH | TTCF_INSLN)
 
+  /* Table of user-settable flags. */
   static const struct flagmap {
     const char *name;
     uint32 acaps, ocaps;
@@ -2561,8 +3682,7 @@ static struct tty *ansi_init(FILE *fp)
     { 0,               0,              0,              0 }
   };
 
-#undef EDIT_OPS
-
+  /* Tables of enumerated setting values. */
   static const struct kw { const char *name; uint32 val; }
     kw_colours[] = {
       { "no",          COLS_NO },
@@ -2574,6 +3694,7 @@ static struct tty *ansi_init(FILE *fp)
       { 0,             0 }
     };
 
+  /* Table of enumerated settings. */
   static const struct enummap {
     const char *name;
     uint32 mask;
@@ -2584,7 +3705,7 @@ static struct tty *ansi_init(FILE *fp)
     { 0,               0,                              0 }
   };
 
-
+  /* Table of recognized terminal types. */
   static const struct termmap {
     const char *pat;
     unsigned acaps, ocaps, tf;
@@ -2692,6 +3813,8 @@ static struct tty *ansi_init(FILE *fp)
 #undef COLS_256
 #undef COLS_16M
 
+#undef EDIT_OPS
+
   union tty_ansiu *u = 0; struct tty *ret = 0;
   const char *term, *config, *p, *l;
   const struct kw *kw;
@@ -2705,71 +3828,101 @@ static struct tty *ansi_init(FILE *fp)
     f = 0;
 #define f_sense 1u
 
+  /* Read the environment variables. */
   config = getenv("MLIB_TTY_ANSICONFIG");
   term = getenv("TERM");
 
+  /* We're not going to touch Emacs's `dumb' terminal. */
   if (term && STRCMP(term, ==, "dumb")) goto end;
 
+  /* Scan the user configuration first.  We'll keep track of masks for the
+   * capabilities that we've set so that we don't clobber them later.
+   */
   if (config) {
     l = config + strlen(config);
     for (;;) {
 
+      /* Skip spaces. */
       for (;;)
        if (config >= l) goto done_config;
        else if (!ISSPACE(*config)) break;
        else config++;
 
+      /* Find the length of the next space-sparated word. */
       for (p = config + 1; p < l && !ISSPACE(*p); p++);
+
       if (*config == '+' || *config == '-') {
+       /* We've found a flag setting. */
+
+       /* Track whether we're supposed to set or clear it. */
        if (*config == '+') f |= f_sense;
        else f &= ~f_sense;
        config++; n = p - config;
 
+       /* Search for the flag name. */
        for (fm = flagmap; fm->name; fm++)
          if (STRNCMP(config, ==, fm->name, n) && !fm->name[n])
            goto found_flag;
        debug("unknown flag `%.*s'", (int)n, config); goto next_config;
+
       found_flag:
+       /* Found it.  Check that we've not already seen this one. */
        if ((acapset&fm->acaps) || (ocapset&fm->ocaps) || (tfset&fm->tf)) {
          debug("duplicate setting for `%s'", fm->name);
          goto next_config;
        }
+
+       /* Apply the setting. */
        if (f&f_sense)
          { acaps |= fm->acaps; ocaps |= fm->ocaps; tf |= fm->tf; }
        acapset |= fm->acaps; ocapset |= fm->ocaps; tfset |= fm->tf;
       } else {
+       /* Must be an assignment of an enumerated setting. */
+
        n = p - config;
+
+       /* Find the `%|=|%' separator. */
        p = memchr(config, '=', n);
        if (!p) {
          debug("missing `=' in setting `%.*s'", (int)n, config);
          goto next_config;
        }
+
+       /* Search for the setting name. */
        nn = p - config;
        for (em = enummap; em->name; em++)
          if (STRNCMP(config, ==, em->name, nn) && !em->name[nn])
            goto found_enum;
        debug("unknown setting `%.*s'", (int)nn, config); goto next_config;
+
       found_enum:
+       /* Found it.  Check that we've not already seen this one. */
+       if (acapset&em->mask) {
+         debug("duplicate setting for `%s'", em->name);
+         goto next_config;
+       }
+
+       /* Now search for the value. */
        p++; nn = n - nn - 1;
        for (kw = em->kw; kw->name; kw++)
          if (STRNCMP(p, ==, kw->name, nn) && !kw->name[nn])
            goto found_kw;
        debug("unknown `%s' value `%.*s", em->name, (int)nn, p);
        goto next_config;
+
       found_kw:
-       if (acapset&em->mask) {
-         debug("duplicate setting for `%s'", em->name);
-         goto next_config;
-       }
+       /* Found that too.  Apply the setting. */
        acaps |= kw->val; acapset |= em->mask;
       }
 
     next_config:
+      /* Move on to the next word. */
       config += n;
     }
   done_config:;
   }
 
+  /* Next, check our built-in table of terminal idiosyncrasies. */
   if (term) {
     for (tm = termmap; tm->pat; tm++)
       if (str_match(tm->pat, term))
@@ -2781,9 +3934,16 @@ static struct tty *ansi_init(FILE *fp)
     tf |= tm->tf&~tfset;
   }
 
+  /* If the user hasn't already set how we handle colour, then check the
+   * environment for more general advice.  Unlike `terminfo'-based backends,
+   * we can handle colour upgrades as well as downgrades.
+   */
   if (!(acapset&TTACF_CSPCMASK)) env_colour_caps(&acaps, ECCF_SET);
-  if (acaps&TTACF_CSPCMASK) ocaps |= TTCF_BGER;
 
+  /* If we can handle background colours, then we can erase to background. */
+  if (acaps&TTACF_BG) ocaps |= TTCF_BGER;
+
+  /* Set up a fresh control block. */
   XNEW(u);
   u->tty.ops = &ansi_ops;
   u->tty.acaps = acaps;
@@ -2792,7 +3952,9 @@ static struct tty *ansi_init(FILE *fp)
   u->tty.wd = 80; u->tty.ht = 25;
   u->tty.st.modes = TTMF_AUTOM | (u->tty.ocaps&TTMF_CVIS);
   u->tty.st.attr.f = 0; u->tty.st.attr.fg = u->tty.st.attr.bg = 0;
-  common_init(&u->ansi.tty, fp);
+  common_init(&u->ansi.tty, fp, 0);
+
+  /* All done. */
   ret = &u->tty; u = 0;
 end:
   xfree(u); return (ret);
@@ -2802,6 +3964,42 @@ end:
 
 /*----- Backend selection -------------------------------------------------*/
 
+/* --- @tty_open@ --- *
+ *
+ * Arguments:  @FILE *fp@ = stream open on terminal, or null
+ *             @unsigned f@ = flags (@TTF_...@)
+ *             @const unsigned *backends@ = ordered list of backends to try
+ *
+ * Returns:    Pointer to terminal control block, or null on error.
+ *
+ * Use:                Open a terminal and return a @struct tty *@ terminal control
+ *             block pointer.
+ *
+ *             If @fp@ is provided, then it will be used for terminal
+ *             output.  If @fp@ is @stdout@, then input (not currently
+ *             supported) will come from @stdin@; otherwise, input comes
+ *             from @fp@.  If @fp@ is null and @TTF_OPEN@ is set, then
+ *             @tty_open@ will attempt to open a terminal for itself: if
+ *             @stdin@ is interactive then it used for input; if @stdout@ --
+ *             or, failing that, @stderr@ -- is interactive, then it is used
+ *             for output; otherwise %|/dev/tty|% is opened and used.  If
+ *             @fp@ is null and @TTF_OPEN@ is not set, then a usable
+ *             terminal control block is still returned, but output cannot
+ *             be sent directly to the terminal -- since there isn't one.
+ *
+ *             If @TTF_BORROW@ is set, then the stream will not be closed by
+ *             @tty_close@.  (This flag is ignored if @fp@ is null.)
+ *
+ *             If @beckends@ is provided, then it points to a vector of
+ *             @TTBK_...@ constants describing the backends to be tried in
+ *             order.  The vector is terminated by @TTBK_END@; if this is
+ *             found, then a null pointer is returned.
+ *
+ *             A null control block pointer is valid for all @tty@
+ *             functions: most will just immediately report failure, but
+ *             there won't be any crashing.
+ */
+
 struct tty *tty_open(FILE *fp, unsigned f, const unsigned *backends)
 {
   static const struct betab {
@@ -2889,6 +4087,17 @@ end:
   return (tty);
 }
 
+/* --- @tty_close@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Closes a terminal, releasing the control block and any
+ *             resources it held.  In particular, if the terminal was opened
+ *             without @TTF_BORROW@, then the output stream is closed.
+ */
+
 void tty_close(struct tty *tty)
 {
   if (tty) {
@@ -2897,6 +4106,18 @@ void tty_close(struct tty *tty)
   }
 }
 
+/* --- @tty_resized@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *
+ * Returns:    Zero if the size hasn't changed, %$+1$% if the size has
+ *             changed, or %$-1$% on error.
+ *
+ * Use:                Update the terminal width and height.  Call this after
+ *             receiving @SIGWINCH@, or otherwise periodically, to avoid
+ *             making a mess.
+ */
+
 int tty_resized(struct tty *tty)
 {
   struct winsize ws;
@@ -2909,6 +4130,30 @@ int tty_resized(struct tty *tty)
 
 /*----- Terminal operations -----------------------------------------------*/
 
+/* --- @tty_setattr@, @tty_setattrg@--- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @const struct tty_attr *a@ = pointer to attributes to set, or
+ *                     null
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Set the indicated formatting attributes on following output.
+ *             If @a@ is null, then clear all attributes, just as if
+ *             @a->f == 0@.
+ *
+ *             If you only ever request attributes which are advertised in
+ *             the terminals capapbility masked, then you'll always get what
+ *             you asked for.  Otherwise, the provided attributes will be
+ *             `clamped', i.e., modified so as to accommodate the terminal's
+ *             shortcomings.  In simple cases, unsupported attributes may
+ *             just be dropped; but they can also be substituted, e.g.,
+ *             single underlining for double, or approximate colours for
+ *             unsupported colours.
+ */
+
 int tty_setattr(struct tty *tty, const struct tty_attr *a)
 {
   struct tty_attr aa;
@@ -2935,6 +4180,30 @@ int tty_setattrg(struct tty *tty,
   }
 }
 
+/* --- @tty_setattrlist@, @tty_setattrlistg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @const struct tty_attrlist *aa@ = pointer to attribute list
+ *                     `menu'
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Search the list for an entry matching the terminal's
+ *             capabilities, i.e., @tty->acaps&a->cap_mask == a->cap_eq@.
+ *             The attributes in the first such entry are set, as if by
+ *             @tty_setattr@.
+ *
+ *             The list is terminated by an entry with @cap_mask == 0@ --
+ *             though it will be checked like any other before ending the
+ *             search.  In particular, this means that an entry with
+ *             @cap_mask == cap_eq == 0@ is a `catch-all', and its
+ *             attributes will be set if no earlier matching entry could be
+ *             found, while an entry with @cap_mask == 0@ and @cap_eq != 0@
+ *             terminates the search without setting any attributes.
+ */
+
 int tty_setattrlist(struct tty *tty, const struct tty_attrlist *aa)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -2953,6 +4222,22 @@ int tty_setattrlistg(struct tty *tty,
       return (0);
 }
 
+/* --- @tty_setmodes@, @tty_setmodesg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @uint32 modes_bic, modes_xor@ = masks to apply to the modes
+ *                     settings
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Adjust the terminal display modes: specifically, the modes
+ *             are adjusted to be @(modes&~modes_bic) ^ modes_xor@.
+ *             Mode bits which aren't supported by the terminal are
+ *             ignored.
+ */
+
 int tty_setmodes(struct tty *tty, uint32 modes_bic, uint32 modes_xor)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -2968,6 +4253,40 @@ int tty_setmodesg(struct tty *tty,
   else return (tty->ops->setmodes(tty, gops, go, modes_bic, modes_xor));
 }
 
+/* --- @tty_move@, @tty_moveg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned orig@ = origin
+ *             @int y, x@ = new cursor position
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Move the cursor.  Coordinates are numbered starting with 0.
+ *
+ *             The @y@ position is interpreted relative to the origin given
+ *             by @orig@: if @TTOF_YCUR@ is set, then the motion is relative
+ *             to the current cursor row; otherwise, it is relative to
+ *             the top `home' row.  Similarly, the @x@ position is
+ *             interpreted relative to the current cursor column if
+ *             @TTOF_XCUR is set, otherwise relative to the leftmost
+ *             column.
+ *
+ *             Not all terminals are capable of all kinds of motions:
+ *             @TTCF_ABSMV@ is set if absolute motion is possible, and
+ *             @TTCF_RELMV@ is set if relative motion is possible.  The
+ *             @TTCF_MIXMV@ bit indicates that the combination of absolute-y
+ *             and relative-x motion is possible; note that the combination
+ *             of relative-y and absolute-x is always possible if relative
+ *             motion is possible at all.
+ *
+ *             The above notwithstanding, all terminals are assumed capable
+ *             of moving the cursor to the start of either the current line
+ *             @tty_move(tty, TTOF_YCUR | TTOF_XHOME, 0, 0)@, or of the next
+ *             line @tty_move(tty, TTOF_YCUR | TTOF_XHOME, +1, 0)@.
+ */
+
 int tty_move(struct tty *tty, unsigned orig, int y, int x)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -2982,6 +4301,21 @@ int tty_moveg(struct tty *tty,
   else return (tty->ops->move(tty, gops, go, orig, y, x));
 }
 
+/* --- @tty_repeat@, @tty_repeatg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int ch@ = character to write
+ *             @unsigned n@ = number of copies
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Write @n@ copies of the character @ch@ to the terminal.
+ *             (Some terminals have a special control sequence for doing
+ *             this.)
+ */
+
 int tty_repeat(struct tty *tty, int ch, unsigned n)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -2996,6 +4330,38 @@ int tty_repeatg(struct tty *tty,
   else return (tty->ops->repeat(tty, gops, go, ch, n));
 }
 
+/* --- @tty_erase@, @tty_eraseg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase portions of the current line or the whole display.
+ *
+ *             If @TTEF_DSP@ is set, then the whole display is affected.  If
+ *             @TTEF_BEGIN@ is set, then the display is erased starting from
+ *             the top left and ending at and including the cursor
+ *             position.  If @TTEF_END@ is set, then the display is erased
+ *             starting from and including the cursor position, and ending
+ *             at the bottom right.  If both flags are set, then,
+ *             additionally, the cursor is moved to its `home' position at
+ *             the top left.
+ *
+ *             If @TTF_DSP@ is not set, then the current line is affected.
+ *             If @TTEF_BEGIN@ is set, then the line is erased starting from
+ *             the left and ending at and including the cursor position.  If
+ *             @TTEF_END@ is set, then the line is erased starting from and
+ *             including the cursor position, and ending at the right hand
+ *             side.
+ *
+ *             If the @TTCF_BGER@ capability is set, then the erased
+ *             positions take on the current background colour; otherwise,
+ *             they have the default background colour.
+ */
+
 int tty_erase(struct tty *tty, unsigned f)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -3010,6 +4376,23 @@ int tty_eraseg(struct tty *tty,
   else return (tty->ops->erase(tty, gops, go, f));
 }
 
+/* --- @tty_erch@, @tty_erchg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned n@ = number of characters to erase
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Erase a number of characters, starting from and including the
+ *             current cursor position.
+ *
+ *             If the @TTCF_BGER@ capability is set, then the erased
+ *             positions take on the current background colour; otherwise,
+ *             they have the default background colour.
+ */
+
 int tty_erch(struct tty *tty, unsigned n)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -3024,6 +4407,26 @@ int tty_erchg(struct tty *tty,
   else return (tty->ops->erch(tty, gops, go, n));
 }
 
+/* --- @tty_ins@, @tty_insg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a number of blank characters or lines.
+ *
+ *             If @TTIDF_LN@ is set, then insert @n@ blank lines above the
+ *             current line.  The cursor must be at the far left of the
+ *             line.
+ *
+ *             Otherwise, insert @n@ empty character spaces at the cursor
+ *             position.
+ */
+
 int tty_ins(struct tty *tty, unsigned f, unsigned n)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -3038,6 +4441,21 @@ int tty_insg(struct tty *tty,
   else return (tty->ops->ins(tty, gops, go, f, n));
 }
 
+/* --- @tty_inch@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @int ch@ = character to insert
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Insert a single character.
+ *
+ *             If the @TTMF_INS@ mode is advertised, then insert mode must
+ *             be set before calling this function.
+ */
+
 int tty_inch(struct tty *tty, int ch)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -3052,6 +4470,25 @@ int tty_inchg(struct tty *tty,
   else return (tty->ops->inch(tty, gops, go, ch));
 }
 
+/* --- @tty_del@, @tty_delg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @unsigned f@ = flags
+ *             @unsigned n@ = number of items to delete
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Delete a number of characters or lines.
+ *
+ *             If @TTIDF_LN@ is set, then delete @n@ blank lines, starting
+ *             with the current line line.  The cursor must be at the far
+ *             left of the line.
+ *
+ *             Otherwise, delete @n@ characters at the cursor position.
+ */
+
 int tty_del(struct tty *tty, unsigned f, unsigned n)
 {
   if (!tty || !tty->fpout) return (-1);
@@ -3066,6 +4503,19 @@ int tty_delg(struct tty *tty,
   else return (tty->ops->del(tty, gops, go, f, n));
 }
 
+/* --- @tty_restore@, @tty_restoreg@ --- *
+ *
+ * Arguments:  @struct tty *tty@ = control block pointer
+ *             @const struct gprintf_ops *gops, void *go@ = output
+ *                     destination
+ *             @const struct tty_state *st@ = state to restore
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Restore the terminal modes and attributes to match a
+ *             state previously captured by copying @tty->st@.
+ */
+
 int tty_restore(struct tty *tty, const struct tty_state *st)
 {
   if (!tty || !tty->fpout) return (-1);