chiark / gitweb /
@@@ tty mess
authorMark Wooding <mdw@distorted.org.uk>
Thu, 24 Apr 2025 18:12:05 +0000 (19:12 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 24 Apr 2025 18:12:05 +0000 (19:12 +0100)
24 files changed:
Makefile.am
configure.ac
struct/buf.3.in
struct/buf.c
struct/buf.h
struct/t/sym-test.c
test/Makefile.am
test/t/tvec-test.c
test/tvec-core.c
test/tvec-output.c
test/tvec-remote.c
test/tvec-types.c
test/tvec-types.h
test/tvec.h
ui/Makefile.am
ui/example/Makefile.am [new file with mode: 0644]
ui/example/progress-test.c [new file with mode: 0644]
ui/tty.c
ui/tty.h
ui/ttycolour.c
ui/ttycolour.h
ui/ttyprogress.c [new file with mode: 0644]
ui/ttyprogress.h [new file with mode: 0644]
utils/macros.h

index f6384599d74567e3936b593031412e10f500f038..2e1f43bfbf6d29673ea40eacdfa3ee6f09f42965 100644 (file)
@@ -106,6 +106,7 @@ SUBDIRS                     += .
 
 ## Examples.
 SUBDIRS                        += test/example
+SUBDIRS                        += ui/example
 
 ###--------------------------------------------------------------------------
 ### Manual.
index 2b7bcd7d2330450b03124c10f8c4469b7e52751b..4d08050549a40deef769f6bb1ef1f836871ed9f8 100644 (file)
@@ -50,7 +50,9 @@ AC_DEFINE_UNQUOTED([SRCDIR], ["$(cd $srcdir && pwd)"],
 dnl--------------------------------------------------------------------------
 dnl C programming environment.
 
+dnl Set the master library list.
 MLIB_LIBS=
+AC_SUBST([MLIB_LIBS])
 
 dnl Headers.
 AC_CHECK_HEADERS([float.h])
@@ -90,9 +92,6 @@ AM_CONDITIONAL([CROSS_COMPILING], [test "$cross_compiling" = yes])
 dnl Floating-point formats.
 mdw_PROBE_FLTFMT
 
-dnl Set the master library list.
-AC_SUBST([MLIB_LIBS])
-
 dnl--------------------------------------------------------------------------
 dnl Terminal characteristics.
 
@@ -103,15 +102,20 @@ AC_ARG_WITH([unibilium],
   [want_unibilium=$withval],
   [want_unibilium=auto])
 
-case $want_unibilium in no) ;; *) have_unibilium=check ;; esac
+case $want_unibilium in
+  no) have_unibilium=no ;;
+  *) have_unibilium=check ;;
+esac
 case $have_unibilium in
   check)
     PKG_CHECK_MODULES([unibilium], [unibilium >= 2.0.0],
-                     [], [have_unibilium=no])
+      [CFLAGS="$CFLAGS $unibilium_CFLAGS"
+       LIBS="$LIBS $unibilium_LIBS"],
+      [have_unibilium=no])
     ;;
 esac
-case $have_unibilium in check) have_unibilium=yes ;; esac
 
+case $have_unibilium in check) have_unibilium=yes ;; esac
 case $want_unibilium,$have_unibilium in
   yes,no)
     AC_MSG_ERROR([`unibilium' library not found but explicitly requested])
@@ -132,7 +136,10 @@ AC_ARG_WITH([terminfo],
   [want_terminfo=$withval],
   [want_terminfo=auto])
 
-case $want_terminfo in no) ;; *) have_terminfo=check ;; esac
+case $want_terminfo in
+  no) have_terminfo=no ;;
+  *) have_terminfo=check ;;
+esac
 case $have_terminfo in
   check) AC_CHECK_HEADERS([term.h], [], [have_terminfo=no]) ;;
 esac
@@ -164,7 +171,10 @@ AC_ARG_WITH([termcap],
   [want_termcap=$withval],
   [want_termcap=auto])
 
-case $want_termcap in no) ;; *) have_termcap=check ;; esac
+case $want_termcap in
+  no) have_termcap=no ;;
+  *) have_termcap=check ;;
+esac
 case $have_termcap in
   check) AC_CHECK_HEADERS([termcap.h], [], [have_termcap=no]) ;;
 esac
@@ -268,10 +278,9 @@ AC_CONFIG_FILES(
   [sel/Makefile]
   [struct/Makefile]
   [sys/Makefile]
-  [test/Makefile]
-  [test/example/Makefile]
+  [test/Makefile] [test/example/Makefile]
   [trace/Makefile]
-  [ui/Makefile]
+  [ui/Makefile] [ui/example/Makefile]
   [utils/Makefile]
   [t/Makefile t/atlocal])
 AC_OUTPUT
index f3c2e7f569b717f8a3d08c17fbfaca3fb4eb0700..6a3a68ca850b72b53eafa24ac857a861fddfd414 100644 (file)
 .\" @dbuf_put
 .\" @dbuf_fill
 .
+.\" @buf_align
 .\" @buf_alignskip
 .\" @buf_alignfill
+.\" @dbuf_align
 .\" @dbuf_alignskip
 .\" @dbuf_alignfill
 .
@@ -545,6 +547,7 @@ and taking a first argument of type
 .BI "int buf_put(buf *" b ", const void *" p ", size_t " sz );
 .BI "int buf_fill(buf *" b ", int " ch ", size_t " sz );
 .PP
+.BI "int buf_align(buf *" b ", size_t " m ", size_t " a ", size_t *" sz_out );
 .BI "int buf_alignskip(buf *" b ", size_t " m ", size_t " a );
 .BI "int buf_alignfill(buf *" b ", int " ch ", size_t " m ", size_t " a );
 .PP
@@ -850,16 +853,33 @@ to the buffer, as if by calling
 If it succeeds, it returns 0; otherwise it returns \-1.
 .PP
 The
-.B buf_alignskip
-function advances the buffer's position as little as possible,
+.B buf_align
+function tries to advance the buffer's position as little as possible,
 so as to cause the position to be
 .I a
 bytes more than a multiple of
-.IR m .
-If
+.IR m ;
+if
 .I m
-is zero then nothing is done.
-The buffer contents are not altered.
+is zero then no change is needed.
+If the buffer is broken, then
+.B buf_align
+immediately returns null.
+Otherwise, it sets
+.BI * sz_out
+to the number of bytes \(en possibly zero \(en
+by which the position would have to advance
+in order to achieve the requested alignment.
+If there is sufficient space remaining in the buffer,
+then the current position is advanced,
+and the old position is returned;
+otherwise, the buffer is broken and a null pointer is returned.
+The
+.B buf_alignskip
+function
+simply advances the buffer position
+until the designed alignment is achieved:
+the buffer contents are not altered.
 The related
 .B buf_alignfill
 function is similar, except that it advances the buffer position by writing
@@ -867,11 +887,17 @@ copies of the byte
 .IR ch ,
 as if by calling
 .BR memset (3).
-Use
+These functions return zero on success,
+or \-1 if the buffer lacked enough space
+or was already broken.
+Usually, it's best to use
 .B buf_alignskip
 for input and
 .B buf_alignfill
 for output.
+The primitive
+.B buf_align
+function can be used for either.
 .
 .SS "Formatted buffer access"
 The function
index 10e05444b9b84051302b0ff719305063d6d69278..81c996f45465081f50ca743ff666519be435ff37 100644 (file)
@@ -238,6 +238,30 @@ static size_t align_step(buf *b, size_t m, size_t a)
   else return ((a + m - BLEN(b)%m)%m);
 }
 
+/* --- @{,d}buf_align@ --- *
+ *
+ * Arguments:  @buf *b@ or @dbuf *db@ = pointer to a buffer block
+ *             @size_t m, a@ = alignment multiple and offset
+ *             @size_t *sz_out@ = where to put the length
+ *
+ * Returns:    Pointer to previous buffer position, or null on error.
+ *
+ * Use:                Advance the buffer position as little as possible such that
+ *             it is @a@ greater than a multiple of @m@, returning the
+ *             (possibly empty) portion of the buffer passed over.
+ */
+
+void *buf_align(buf *b, size_t m, size_t a, size_t *sz_out)
+{
+  size_t sz;
+
+  if (BBAD(b)) return (0);
+  sz = align_step(b, m, a); *sz_out = sz;
+  return (buf_get(b, sz));
+}
+void *(dbuf_align)(dbuf *db, size_t m, size_t a, size_t *sz_out)
+  { return (dbuf_align(db, m, a, sz_out)); }
+
 /* --- @{,d}buf_alignskip@ --- *
  *
  * Arguments:  @buf *b@ or @dbuf *db@ = pointer to a buffer block
index 8201c49f1bddc06216a2e8180d5dc8ae4e4e179f..e169aa8b8b6ae8eefc207f3ffb90db8b83ccb0ed 100644 (file)
@@ -289,6 +289,26 @@ extern int buf_fill(buf */*b*/, int /*ch*/, size_t /*sz*/);
 extern int dbuf_fill(dbuf */*db*/, int /*ch*/, size_t /*sz*/);
 #define dbuf_fill(db, ch, sz) (buf_fill(DBUF_BUF(db), (ch), (sz)))
 
+/* --- @{,d}buf_align@ --- *
+ *
+ * Arguments:  @buf *b@ or @dbuf *db@ = pointer to a buffer block
+ *             @size_t m, a@ = alignment multiple and offset
+ *             @size_t *sz_out@ = where to put the length
+ *
+ * Returns:    Pointer to previous buffer position, or null on error.
+ *
+ * Use:                Advance the buffer position as little as possible such that
+ *             it is @a@ greater than a multiple of @m@, returning the
+ *             (possibly empty) portion of the buffer passed over.
+ */
+
+extern void *buf_align(buf */*b*/, size_t /*m*/, size_t /*a*/,
+                      size_t */*sz_out*/);
+extern void *dbuf_align(dbuf */*db*/, size_t /*m*/, size_t /*a*/,
+                       size_t */*sz_out*/);
+#define dbuf_align(db, m, a, sz_out)                                   \
+  (buf_align(DBUF_BUF(db), (m), (a), (sz_out)))
+
 /* --- @{,d}buf_alignskip@ --- *
  *
  * Arguments:  @buf *b@ or @dbuf *db@ = pointer to a buffer block
index 1862a8742028028ccd114979f124ded7dfe8a646..5bbbff80d09b5ac60bda69108e4646ac8d038e1f 100644 (file)
@@ -5,6 +5,7 @@
 
 #include "macros.h"
 #include "sym.h"
+#include "unihash.h"
 
 typedef struct word {
   sym_base _b;
@@ -57,6 +58,9 @@ int main(void)
        puts("*MISSING*");
     } else if (STRCMP(p, ==, "count")) {
       printf("%lu\n", (unsigned long)n);
+    } else if (STRCMP(p, ==, "seed")) {
+      char *k = strtok(0, " ");
+      unihash_setkey(&unihash_global, strtoul(k, 0, 0));
     } else if (STRCMP(p, ==, "show")) {
       sym_iter i;
       word *w;
index fabb0d7199a9d53197e96664a7c33e66a5e95a28..ebb057cfb5501c9fcc2223bf565202a3957b8f0f 100644 (file)
@@ -29,8 +29,6 @@ include $(top_srcdir)/vars.am
 noinst_LTLIBRARIES      = libtest.la
 libtest_la_SOURCES      =
 
-SUBDIRS                         =
-
 ###--------------------------------------------------------------------------
 ### Component files.
 
index a86f23022055453b3c77d103767a936601f8ebf0..3840e800bb26557fa740981bb74e4f36ee1aa856 100644 (file)
@@ -44,28 +44,28 @@ static const struct tvec_iassoc ienum_assocs[] = {
   { "less",           -1 },
   { "equal",           0 },
   { "greater",        +1 },
-  { 0 }
+  TVEC_ENDENUM
 };
 
 static const struct tvec_uassoc uenum_assocs[] = {
   { "apple",           0 },
   { "banana",          1 },
   { "clementine",      2 },
-  { 0 }
+  TVEC_ENDENUM
 };
 
 static const struct tvec_fassoc fenum_assocs[] = {
   { "e",               2.718281828459045 },
   { "pi",              3.141592653589793 },
   { "tau",             6.283185307179586 },
-  { 0 }
+  TVEC_ENDENUM
 };
 
 static const struct tvec_passoc penum_assocs[] = {
   { "alice",           &uenum_assocs[0] },
   { "bob",             &uenum_assocs[1] },
   { "carol",           &uenum_assocs[2] },
-  { 0 }
+  TVEC_ENDENUM
 };
 
 #if __STDC_VERSION__ >= 199901
@@ -114,7 +114,7 @@ static const struct tvec_flag attr_flags[] = {
   { "bright",          0x40, 0x40 },
   { "flash",           0x80, 0x80 },
 
-  { 0 }
+  TVEC_ENDFLAGS
 };
 
 static const struct tvec_flaginfo attr_info =
index 6b54f0a9dd1e38eaac83dde0c0bb2ba529090fda..7a65c519f1cb4a44a12a1a567c402182d605aea4 100644 (file)
@@ -58,7 +58,7 @@ const char *tvec_strlevel(unsigned level)
 {
   switch (level) {
 #define CASE(tag, name, val)                                           \
-    case TVLEV_##tag: return (name);
+    case TVLV_##tag: return (name);
     TVEC_LEVELS(CASE)
 #undef CASE
     default: return ("??");
@@ -74,7 +74,7 @@ const char *tvec_strlevel(unsigned level)
  * Returns:    ---
  *
  * Use:                Report an message with a given severity.  Messages with level
- *             @TVLEV_ERR@ or higher force a nonzero exit code.
+ *             @TVLV_ERR@ or higher force a nonzero exit code.
  */
 
 void tvec_report(struct tvec_state *tv, unsigned level, const char *msg, ...)
@@ -88,7 +88,7 @@ void tvec_report_v(struct tvec_state *tv, unsigned level,
                   const char *msg, va_list *ap)
 {
   tv->output->ops->report(tv->output, level, msg, ap);
-  if (level >= TVLEV_ERR) tv->f |= TVSF_ERROR;
+  if (level >= TVLV_ERR) tv->f |= TVSF_ERROR;
 }
 
 /* --- @tvec_error@, @tvec_notice@, @tvec_info@ --- *
@@ -116,7 +116,7 @@ int tvec_error(struct tvec_state *tv, const char *msg, ...)
 {
   va_list ap;
 
-  va_start(ap, msg); tvec_report_v(tv, TVLEV_ERR, msg, &ap); va_end(ap);
+  va_start(ap, msg); tvec_report_v(tv, TVLV_ERR, msg, &ap); va_end(ap);
   return (-1);
 }
 
@@ -124,14 +124,14 @@ void tvec_notice(struct tvec_state *tv, const char *msg, ...)
 {
   va_list ap;
 
-  va_start(ap, msg); tvec_report_v(tv, TVLEV_NOTE, msg, &ap); va_end(ap);
+  va_start(ap, msg); tvec_report_v(tv, TVLV_NOTE, msg, &ap); va_end(ap);
 }
 
 void tvec_info(struct tvec_state *tv, const char *msg, ...)
 {
   va_list ap;
 
-  va_start(ap, msg); tvec_report_v(tv, TVLEV_INFO, msg, &ap); va_end(ap);
+  va_start(ap, msg); tvec_report_v(tv, TVLV_INFO, msg, &ap); va_end(ap);
 }
 
 /* --- @tvec_outputext@ --- *
index 014423e0a831defd07a54ae0be9398ec322f430e..40ab769868fa396730301b3afa45f86f2cd25eb3 100644 (file)
@@ -44,6 +44,7 @@
 #include "macros.h"
 #include "quis.h"
 #include "report.h"
+#include "tty.h"
 #include "ttycolour.h"
 
 #include "tvec.h"
@@ -80,7 +81,7 @@ static const char *regdisp(unsigned disp)
  *             @const char *what, ...@ = format string describing where the
  *                     setting came from
  *
- * Returns:    @0@ if the string, or variable,  is set to something
+ * Returns:    @0@ if the string, or variable, is set to something
  *             falseish, @1@ if it's set to something truish, or @dflt@
  *             otherwise.
  */
@@ -481,15 +482,57 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
 
 /*----- Human-readable output ---------------------------------------------*/
 
+#define GENATTR(want, attrs, fgspc, fgcol, bgspc, bgcol)               \
+  { (want), (want),                                                    \
+    { ((fgspc) << TTAF_FGSPCSHIFT) |                                   \
+      ((bgspc) << TTAF_BGSPCSHIFT) | (attrs),                          \
+      0, (fgcol), (bgcol) } }
+
+#define FGBG(want, attrs, fgcol, bgcol)                                        \
+  GENATTR(want | TTACF_FG | TTACF_BG, attrs,                           \
+         TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_1BPCBR, TTCOL_##bgcol)
+#define FG(want, attrs, fgcol)                                         \
+  GENATTR(want | TTACF_FG, attrs,                                      \
+         TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_NONE, 0)
+#define BG(want, attrs, bgcol)                                         \
+  GENATTR(want | TTACF_BG, attrs,                                      \
+         TTCSPC_NONE, 0, TTCSPC_1BPCBR, TTCOL_##bgcol)
+#define ATTR(want, attrs)                                              \
+  GENATTR(want, attrs, TTCSPC_NONE, 0, TTCSPC_NONE, 0)
+
+#define BOLD (TTWT_BOLD << TTAF_WTSHIFT)
+
+static const struct tty_attrlist
+  loc_attrs[] = { FG(0, 0, CYN), TTY_ATTRLIST_CLEAR },
+  locsep_attrs[] = { FG(0, 0, BRBLU), TTY_ATTRLIST_CLEAR },
+  note_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  err_attrs[] = { FG(0, BOLD, MGN), TTY_ATTRLIST_CLEAR },
+  unklv_attrs[] = { FGBG(0, BOLD, BRWHT, BRRED), ATTR(0, TTAF_INVV) },
+  vfound_attrs[] = { FG(0, 0, RED), TTY_ATTRLIST_CLEAR },
+  vexpect_attrs[] = { FG(0, 0, GRN), TTY_ATTRLIST_CLEAR },
+  vunset_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  lose_attrs[] = { FG(0, BOLD, RED), ATTR(0, BOLD | TTAF_INVV) },
+  skip_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  xfail_attrs[] = { FG(0, BOLD, BLU), ATTR(0, BOLD) },
+  win_attrs[] = { FG(0, 0, GRN), TTY_ATTRLIST_CLEAR },
+  sblose_attrs[] = { FG(0, BOLD, RED), ATTR(0, BOLD) };
+
+#undef GENATTR
+#undef FGBG
+#undef FG
+#undef BG
+#undef ATTR
+#undef BOLD
+
 /* Predefined attributes. */
 #define HIGHLIGHTS(_st, _)                                             \
-  _(_st, LOCFN,           "lf", TC_FG(CYAN))   /* location filename */         \
-  _(_st, LOCLN,           "ln", TC_FG(CYAN))   /* location line number */      \
-  _(_st, LOCSEP,   "ls", TC_FG(BRBLUE))        /* location separator `:' */    \
+  _(_st, LOCFN,           "lf", loc_attrs)     /* location filename */         \
+  _(_st, LOCLN,           "ln", loc_attrs)     /* location line number */      \
+  _(_st, LOCSEP,   "ls", locsep_attrs) /* location separator `:' */    \
   _(_st, INFO,    "mi", 0)             /* information */               \
-  _(_st, NOTE,    "mn", TC_FG(YELLOW)) /* notices */                   \
-  _(_st, ERR,     "me", TC_FG(MAGENTA) | TCAF_BOLD) /* error messages */       \
-  _(_st, UNKLEV,   "mu", TC_FG(WHITE) | TC_BG(RED) | TCAF_BOLD)                \
+  _(_st, NOTE,    "mn", note_attrs)    /* notices */                   \
+  _(_st, ERR,     "me", err_attrs)     /* error messages */            \
+  _(_st, UNKLV,           "mu", unklv_attrs)   /* unknown-level messages */    \
   _(_st, DSINPUT,  "di", 0)            /* disposition for input value */ \
   _(_st, DSOUTPUT, "do", 0)            /* ... unsolicited output */    \
   _(_st, DSMATCH,  "dm", 0)            /* ... matching output */       \
@@ -503,16 +546,16 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
   _(_st, VINPUT,   "vi", 0)            /* input value */               \
   _(_st, VOUTPUT,  "vo", 0)            /* unsolicited output value */  \
   _(_st, VMATCH,   "vm", 0)            /* matching output value */     \
-  _(_st, VFOUND,   "vf", TC_FG(BRRED)) /* incorrect output value */    \
-  _(_st, VEXPECT,  "vx", TC_FG(GREEN)) /* reference output value */    \
-  _(_st, VUNSET,   "vu", TC_FG(YELLOW))        /* register not set */          \
-  _(_st, LOSE,    "ol", TC_FG(RED) | TCAF_BOLD) /* report failure */   \
-  _(_st, SKIP,    "os", TC_FG(YELLOW)) /* report a skipped test/group */ \
-  _(_st, XFAIL,           "ox", TC_FG(BLUE) | TCAF_BOLD) /* report expected fail */ \
-  _(_st, WIN,     "ow", TC_FG(GREEN))  /* report success */            \
-  _(_st, SBLOSE,   "sl", TC_FG(RED) | TCAF_BOLD) /* scoreboard failure */      \
-  _(_st, SBSKIP,   "ss", TC_FG(YELLOW))        /* scoreboard skipped test */   \
-  _(_st, SBXFAIL,  "sx", TC_FG(BLUE) | TCAF_BOLD) /* scoreboard xfail */       \
+  _(_st, VFOUND,   "vf", vfound_attrs) /* incorrect output value */    \
+  _(_st, VEXPECT,  "vx", vexpect_attrs)        /* reference output value */    \
+  _(_st, VUNSET,   "vu", vunset_attrs) /* register not set */          \
+  _(_st, LOSE,    "ol", lose_attrs)    /* report failure */            \
+  _(_st, SKIP,    "os", skip_attrs)    /* report a skipped test/group */ \
+  _(_st, XFAIL,           "ox", xfail_attrs)   /* report expected fail */      \
+  _(_st, WIN,     "ow", win_attrs)     /* report success */            \
+  _(_st, SBLOSE,   "sl", sblose_attrs) /* scoreboard failure */        \
+  _(_st, SBSKIP,   "ss", skip_attrs)   /* scoreboard skipped test */   \
+  _(_st, SBXFAIL,  "sx", xfail_attrs)  /* scoreboard xfail */          \
   _(_st, SBWIN,           "sw", 0)             /* scoreboard success */
 
 TTYCOLOUR_DEFENUM(HIGHLIGHTS, HL_);
@@ -525,11 +568,11 @@ struct human_output {
   struct tvec_output _o;               /* output base class */
   struct tvec_state *tv;               /* stashed testing state */
   arena *a;                            /* arena for memory allocation */
+  struct tty *tty;                     /* output terminal, or null */
   struct layout lyt;                   /* output layout */
   char *outbuf; size_t outsz;          /* buffer for formatted output */
   dstr scoreboard;                     /* history of test group results */
-  unsigned short attr[HL__LIMIT];      /* highlight attribute map */
-  struct ttycolour_state tc;           /* terminal colour state */
+  struct tty_attr attr[HL__LIMIT];     /* highlight attribute map */
   int maxlen;                          /* longest register name */
   unsigned f;                          /* flags */
                                        /*   bits 0--7 from @TVHF_...@ */
@@ -552,7 +595,7 @@ static void setattr_common(struct human_output *h,
                           const struct gprintf_ops *gops, void *go, int hl)
 {
   if (h->f&TVHF_COLOUR)
-    ttycolour_setattr(gops, go, &h->tc, hl < 0 ? 0 : h->attr[hl]);
+    tty_setattrg(h->tty, gops, go, hl >= 0 ? &h->attr[hl] : 0);
 }
 
 static void setattr(struct human_output *h, int hl)
@@ -1018,7 +1061,7 @@ static void human_etest(struct tvec_output *o, unsigned outcome)
  *
  * Arguments:  @struct tvec_output *o@ = output sink, secretly a
  *                      @struct human_output@
- *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @unsigned level@ = message level (@TVLV_...@)
  *             @const char *msg@, @va_list *ap@ = format string and
  *                     arguments
  *
@@ -1045,9 +1088,9 @@ static void human_report(struct tvec_output *o, unsigned level,
 
   switch (level) {
 #define CASE(tag, name, val)                                           \
-    case TVLEV_##tag: levstr = name; levhl = HL_##tag; break;
+    case TVLV_##tag: levstr = name; levhl = HL_##tag; break;
     TVEC_LEVELS(CASE)
-    default: levstr = "??"; levhl = HL_UNKLEV; break;
+    default: levstr = "??"; levhl = HL_UNKLV; break;
   }
 
   if (h->f&HOF_PROGRESS) { clear_progress(h); f |= f_progress; }
@@ -1157,6 +1200,7 @@ static void human_destroy(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
 
+  tty_close(h->tty);
   destroy_layout(&h->lyt,
                 h->lyt.fp == stdout || h->lyt.fp == stderr ? 0 : DLF_CLOSE);
   dstr_destroy(&h->scoreboard);
@@ -1205,7 +1249,6 @@ static const struct tvec_outops human_ops = {
 struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
 {
   struct human_output *h;
-  const char *p;
   struct stat st_out, st_err;
   int rc_out, rc_err;
 
@@ -1215,7 +1258,7 @@ struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
   assert(!(f&~m));
 
   if (!(m&TVHF_TTY))
-    switch (getenv_boolean("TVEC_TTY", -1)) {
+    switch (getenv_boolean("MLIB_TVEC_TTY", -1)) {
       case 1: f |= TVHF_TTY; break;
       case 0: break;
       default:
@@ -1223,14 +1266,12 @@ struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
        break;
     }
   if (!(m&TVHF_COLOUR))
-    switch (getenv_boolean("TVEC_COLOUR", -1)) {
+    switch (getenv_boolean("MLIB_TVEC_COLOUR", -1)) {
       case 1: f |= TVHF_COLOUR; break;
       case 0: break;
       default:
-       if (f&TVHF_TTY) {
-         p = getenv("TERM");
-         if (p && STRCMP(p, !=, "dumb")) f |= TVHF_COLOUR;
-       }
+       if (ttycolour_enablep((f&TVHF_TTY ? TCEF_TTY : 0) | TCEF_DFLT))
+         f |= TVHF_COLOUR;
        break;
     }
 
@@ -1252,10 +1293,15 @@ struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
   h->f = f;
 
   /* Initialize the colour tables. */
-  if (h->f&TVHF_COLOUR) {
-    ttycolour_config(h->attr, "TVEC_COLOURS", TCIF_GETENV | TCIF_REPORT,
-                    hltab);
-    ttycolour_init(&h->tc);
+  if (!(h->f&TVHF_COLOUR))
+    h->tty = 0;
+  else {
+    h->tty = tty_open(fp, TTF_BORROW, 0);
+    if (!h->tty)
+      h->f &= ~TVHF_COLOUR;
+    else
+      ttycolour_config(h->attr, "MLIB_TVEC_COLOURS",
+                      TCIF_GETENV | TCIF_REPORT, h->tty, hltab);
   }
 
   init_layout(&h->lyt, fp, 0);
@@ -1672,7 +1718,7 @@ static void machine_etest(struct tvec_output *o, unsigned outcome)
  *
  * Arguments:  @struct tvec_output *o@ = output sink, secretly a
  *                      @struct machine_output@
- *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @unsigned level@ = message level (@TVLV_...@)
  *             @const char *msg@, @va_list *ap@ = format string and
  *                     arguments
  *
@@ -2123,7 +2169,7 @@ static void tap_etest(struct tvec_output *o, unsigned outcome)
  *
  * Arguments:  @struct tvec_output *o@ = output sink, secretly a
  *                      @struct tap_output@
- *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @unsigned level@ = message level (@TVLV_...@)
  *             @const char *msg@, @va_list *ap@ = format string and
  *                     arguments
  *
@@ -2504,7 +2550,7 @@ static void am_etest(struct tvec_output *o, unsigned outcome)
  *
  * Arguments:  @struct tvec_output *o@ = output sink, secretly a
  *                      @struct automake_output@
- *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @unsigned level@ = message level (@TVLV_...@)
  *             @const char *msg@, @va_list *ap@ = format string and
  *                     arguments
  *
@@ -2664,10 +2710,10 @@ struct tvec_output *tvec_amoutput(const struct tvec_amargs *a)
 
 struct tvec_output *tvec_dfltoutput(FILE *fp)
 {
-  int ttyp = getenv_boolean("TVEC_TTY", -1);
+  int ttyp = getenv_boolean("MLIB_TVEC_TTY", -1);
 
   if (ttyp == -1) ttyp = isatty(fileno(fp));
-  if (ttyp) return (tvec_humanoutput(fp, 0, 0));
+  if (ttyp) return (tvec_humanoutput(fp, TVHF_TTY, TVHF_TTY));
   else return (tvec_machineoutput(fp));
 }
 
index bcbfad0d3af27b380417b1c90621a55633b69916..637f851efde34a2f00cc3c4a73286874671b246b 100644 (file)
@@ -169,7 +169,7 @@ static PRINTF_LIKE(3, 4)
 
   va_start(ap, msg);
   close_comms(rc); rc->f |= TVRF_BROKEN;
-  tvec_report_v(tv, TVLEV_ERR, msg, &ap);
+  tvec_report_v(tv, TVLV_ERR, msg, &ap);
   va_end(ap);
   return (-1);
 }
@@ -1020,7 +1020,7 @@ static void remote_etest(struct tvec_output *o, unsigned outcome)
 /* --- @remote_report@ --- *
  *
  * Arguments:  @struct tvec_output *o@ = output sink (ignored)
- *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @unsigned level@ = message level (@TVLV_...@)
  *             @const char *msg@, @va_list *ap@ = format string and
  *                     arguments
  *
@@ -1662,7 +1662,7 @@ static void report_errline(char *p, size_t n, void *ctx)
  *
  *             If @f@ has @ERF_SILENT@ set, then discard the stderr material
  *             without reporting it.  Otherwise it is reported as
- *             @TVLEV_NOTE@.
+ *             @TVLV_NOTE@.
  *
  *             if @f@ has @ERF_CLOSE@ set, then continue reading until
  *             end-of-file is received; also, report any final partial line,
index 9e7a35fe5f76b07f35b319a6a39e50613aea89e2..70ce87aad71d29441d5c7482758c3935d9ae9c09 100644 (file)
@@ -3534,7 +3534,7 @@ static void dump_bytes(const union tvec_regval *rv,
 {
   const unsigned char *p = rv->bytes.p, *l = p + rv->bytes.sz;
   size_t off, sz = rv->bytes.sz;
-  unsigned i, n;
+  unsigned i, n, w;
   int wd;
 
   if (!rv->text.sz) { dump_empty("bytes", style, gops, go); return; }
@@ -3545,17 +3545,18 @@ static void dump_bytes(const union tvec_regval *rv,
     return;
   }
 
-  if (sz > 16) gprintf(gops, go, "\n\t");
+  if (sz <= 16) w = sz;
+  else { gprintf(gops, go, "\n\t"); w = 16; }
 
   off = 0; wd = hex_width(sz);
   while (p < l) {
     if (l - p < 16) n = l - p;
     else n = 16;
 
-    for (i = 0; i < n; i++) {
+    for (i = 0; i < w; i++) {
       if (i < n) gprintf(gops, go, "%02x", p[i]);
       else gprintf(gops, go, "  ");
-      if (i < n - 1 && i%4 == 3) gprintf(gops, go, " ");
+      if (i < w - 1 && i%4 == 3) gprintf(gops, go, " ");
     }
     gprintf(gops, go, " ; ");
     if (sz > 16) gprintf(gops, go, "[%0*lx] ", wd, (unsigned long)off);
@@ -3975,7 +3976,7 @@ void tvec_initbuffer(union tvec_regval *rv,
 
 void tvec_allocbuffer(union tvec_regval *rv)
 {
-  unsigned char *p; size_t n;
+  unsigned char *p;
   size_t m = rv->buf.m, a = rv->buf.a, off;
 
   if (rv->buf.p) free(rv->buf.p - rv->buf.off);
index 5076482ea5d0ef25a1157bd341c9de290e0c5cbd..a56a7fe410bd172c66fb2d9a5847a32726f639af 100644 (file)
@@ -438,7 +438,7 @@ TVEC_MISCSLOTS(DEFINFO)
 extern const struct tvec_ienuminfo tvenum_bool;
 extern const struct tvec_ienuminfo tvenum_cmp;
 
-/* --- @tvec_claimeq_tenum@, @TVEC_CLAIMEQ_TENUM@ --- *
+/* --- @tvec_claimeq_tenum@, @TVEC_CLAIMEQ_TENUM@, @TVEC_CLAIMEQ_PTR@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
  *             @const struct tvec_typeenuminfo *ei@ = enumeration type info
@@ -455,10 +455,16 @@ extern const struct tvec_ienuminfo tvenum_cmp;
  *             mismatched values are dumped: @t0@ is printed as the output
  *             value and @t1@ is printed as the input reference.
  *
+ *             The @ei@ argument to @tvec_claimeq_penum@ (but not the other
+ *             functions) may be null, as a hack for comparing two plain
+ *             pointers.
+ *
  *             The @TVEC_CLAIM_TENUM@ macro is similar, only it (a)
  *             identifies the file and line number of the call site
  *             automatically, and (b) implicitly quotes the source text of
- *             the @t0@ and @t1@ arguments in the failure message.
+ *             the @t0@ and @t1@ arguments in the failure message.  The
+ *             @TVEC_CLAIMEQ_PTR@ macro simply compares two pointers in the
+ *             same way.
  */
 
 #define DECLCLAIM(tag, ty, slot)                                       \
@@ -481,6 +487,9 @@ TVEC_MISCSLOTS(DECLCLAIM)
 #define TVEC_CLAIMEQ_PENUM(tv, ei, p0, p1)                             \
        (tvec_claimeq_penum(tv, ei, p0, p1,                             \
                            __FILE__, __LINE__, #p0 " /= " #p1))
+#define TVEC_CLAIMEQ_PTR(tv, p0, p1)                                   \
+       (tvec_claimeq_penum(tv, 0, p0, p1,                              \
+                           __FILE__, __LINE__, #p0 " /= " #p1))
 
 /*----- Flags type --------------------------------------------------------*/
 
index d5c1a3f8e5af31ddc07e786e52c67a20476a5809..8a185393e8b84c3ec03cfb79b87fa0583509147f 100644 (file)
@@ -853,7 +853,7 @@ extern const char *tvec_strlevel(unsigned /*level*/);
  * Returns:    ---
  *
  * Use:                Report an message with a given severity.  Messages with level
- *             @TVLEV_ERR@ or higher force a nonzero exit code.
+ *             @TVLV_ERR@ or higher force a nonzero exit code.
  */
 
 #define TVEC_LEVELS(_)                                                 \
@@ -861,10 +861,10 @@ extern const char *tvec_strlevel(unsigned /*level*/);
        _(NOTE, "notice", 4)                                            \
        _(ERR, "ERROR", 7)
 enum {
-#define TVEC_DEFLEVEL(tag, name, val) TVLEV_##tag = val,
+#define TVEC_DEFLEVEL(tag, name, val) TVLV_##tag = val,
   TVEC_LEVELS(TVEC_DEFLEVEL)
 #undef TVEC_DEFLEVEL
-  TVLEV_LIMIT
+  TVLV_LIMIT
 };
 
 extern PRINTF_LIKE(3, 4)
index c44589f8c6d3c71bb393a1c51780323f4898ba7a..ca6d9e8c120094e4681951cc7e4a511697404cea 100644 (file)
@@ -54,10 +54,20 @@ libui_la_SOURCES    += report.c
 EXTRA_DIST             += report.3.in
 LIBMANS                        += report.3
 
-## Terminal colours.
+## Terminal handling.
+pkginclude_HEADERS     += tty.h
+libui_la_SOURCES       += tty.c
+##EXTRA_DIST           += tty.3.in
+##LIBMANS              += tty.3
+
 pkginclude_HEADERS     += ttycolour.h
 libui_la_SOURCES       += ttycolour.c
 ##EXTRA_DIST           += ttycolour.3.in
 ##LIBMANS              += ttycolour.3
 
+pkginclude_HEADERS     += ttyprogress.h
+libui_la_SOURCES       += ttyprogress.c
+##EXTRA_DIST           += ttyprogress.3.in
+##LIBMANS              += ttyprogress.3
+
 ###----- That's all, folks --------------------------------------------------
diff --git a/ui/example/Makefile.am b/ui/example/Makefile.am
new file mode 100644 (file)
index 0000000..a7dd8ba
--- /dev/null
@@ -0,0 +1,37 @@
+### -*-makefile-*-
+###
+### Build file for test vector example
+###
+### (c) 2024 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the mLib utilities library.
+###
+### mLib is free software: you can redistribute it and/or modify it under
+### the terms of the GNU Library General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or (at
+### your option) any later version.
+###
+### mLib is distributed in the hope that it will be useful, but WITHOUT
+### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+### License for more details.
+###
+### You should have received a copy of the GNU Library General Public
+### License along with mLib.  If not, write to the Free Software
+### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+### USA.
+
+include $(top_srcdir)/vars.am
+
+noinst_PROGRAMS                 =
+
+###--------------------------------------------------------------------------
+### The test vector framework example.
+
+noinst_PROGRAMS                += progress-test
+progress_test_SOURCES   = progress-test.c
+
+###----- That's all, folks --------------------------------------------------
diff --git a/ui/example/progress-test.c b/ui/example/progress-test.c
new file mode 100644 (file)
index 0000000..53c7c9f
--- /dev/null
@@ -0,0 +1,67 @@
+#include <locale.h>
+#include <time.h>
+
+#include <unistd.h>
+
+#include "tty.h"
+#include "ttyprogress.h"
+
+static unsigned pos;
+static const char *left, *right;
+
+static void render_bar(struct ttyprogress_item *item,
+                      struct ttyprogress_render *render)
+{
+  ttyprogress_putleft(render, " %u%%", pos/3);
+  if (left) ttyprogress_putleft(render, " %s", left);
+  ttyprogress_putright(render, " %u ", pos);
+  if (right) ttyprogress_putright(render, "%s ", right);
+  ttyprogress_showbar(render, pos/300.0);
+}
+
+static void render_sub(struct ttyprogress_item *item,
+                      struct ttyprogress_render *render)
+{
+  unsigned subpos = pos%30;
+
+  ttyprogress_putleft(render, " %u%%", (10*subpos + 1)/3);
+  ttyprogress_putright(render, " %u ", subpos);
+  ttyprogress_showbar(render, subpos/30.0);
+}
+
+int main(int argc, char *argv[])
+{
+  struct tty *tty;
+  struct ttyprogress progress;
+  struct ttyprogress_item
+    bar = TTYPROGRESS_ITEM_INIT,
+    sub = TTYPROGRESS_ITEM_INIT;
+
+  setlocale(LC_ALL, "");
+
+  if (argc >= 2) left = argv[1];
+  if (argc >= 3) right = argv[2];
+
+  tty = tty_open(stdout, TTF_BORROW, 0);
+  if (ttyprogress_init(&progress, tty))
+    fprintf(stderr, "terminal insufficiently capable\n");
+
+  bar.render = render_bar; ttyprogress_additem(&progress, &bar);
+  for (pos = 0; pos <= 300; pos++) {
+    if (pos%30)
+      ;
+    else if (pos%60)
+      { sub.render = render_sub; ttyprogress_additem(&progress, &sub); }
+    else
+      ttyprogress_removeitem(&progress, &sub);
+    ttyprogress_update(&progress);
+    usleep(100000);
+  }
+  ttyprogress_removeitem(&progress, &bar);
+  ttyprogress_update(&progress);
+
+  ttyprogress_clear(&progress);
+  tty_close(tty);
+
+  return (0);
+}
index 77e778af85907e86879fabd2d630ea50478a5e21..f3f526fa0d0de15dd12e38f4fa9e778b74274d6b 100644 (file)
--- a/ui/tty.c
+++ b/ui/tty.c
 
 #include "config.h"
 
+#include <ctype.h>
 #include <errno.h>
+#include <limits.h>
 #include <stdio.h>
+#include <string.h>
 
+#include <sys/types.h>
+#include <sys/time.h>
 #include <unistd.h>
 #include <termios.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
 
 #ifdef HAVE_TERMCAP
 #  include <termcap.h>
 #endif
 
 #ifdef HAVE_TERMINFO
+#  include <curses.h>
 #  include <term.h>
+#  undef erase
+#  undef inch
+#  undef move
 #endif
 
 #ifdef HAVE_UNIBILIUM
 #  include <unibilium.h>
 #endif
 
+#include "alloc.h"
 #include "gprintf.h"
 #include "macros.h"
+#include "str.h"
 #include "tty.h"
 
-/*----- Data structures ---------------------------------------------------*/
-
 /*----- Common support machinery ------------------------------------------*/
 
-static uint32 basic_colour_map[] = {
-  /* standard black    */ 0x000000,
-  /* standard red      */ 0xcc0000,
-  /* standard green    */ 0x00cc00,
-  /* standard yellow   */ 0xcccc00,
-  /* standard blue     */ 0x0000cc,
-  /* standard magenta  */ 0xcc00cc,
-  /* standard cyan     */ 0x00cccc,
-  /* standard white    */ 0xcccccc
-}, bright_colour_map[] = {
-  /* bright black      */ 0x333333,
-  /* bright red                */ 0xff3333,
-  /* bright green      */ 0x33ff33,
-  /* bright yellow     */ 0xffff33,
-  /* bright blue       */ 0x3333ff,
-  /* bright magenta    */ 0xff33ff,
-  /* bright cyan       */ 0x33ffff,
-  /* bright white      */ 0xffffff
-};
+/* --- @debug@ --- *
+ *
+ * Arguments:   @const char *fmt@ = format control string
+ *              @...@ = format arguemnts
+ *
+ * Returns:     ---
+ *
+ * Use:         Maybe report a debugging message to standard error.
+ */
 
-static void clamp_colours(uint32 *space_out, uint32 *colour_out,
-                         uint32 space, uint32 colour, uint32 acaps)
+static PRINTF_LIKE(1, 2) void debug(const char *fmt, ...)
+{
+  const char *p;
+  va_list ap;
+
+  p = getenv("MLIB_TTY_DEBUG");
+  if (p && *p != 'n' && *p != '0') {
+    va_start(ap, fmt);
+    fputs("mLib TTY: ", stderr);
+    vfprintf(stderr, fmt, ap);
+    fputc('\n', stderr);
+    va_end(ap);
+  }
+}
+
+static void common_init(struct tty *tty, FILE *fp)
+{
+  static const struct baudtab { speed_t code; unsigned baud; } baudtab[] = {
+  /*
+     ;;; The baud-rate table is very boring to type.  To make life less
+     ;;; awful, put the rates in this list and evaluate the code to get Emacs
+     ;;; to regenerate it.
+
+     (let ((bauds '(50 75 110 134 150 200 300 600 1200 1800 2400 4800 9600
+                   19200 38400 57600 115200 230400 460800 500000 576000
+                   921600 1000000 1152000 1500000 2000000 2500000 3000000
+                   3500000 4000000)))
+       (save-excursion
+        (goto-char (point-min))
+        (search-forward (concat "***" "BEGIN baudlist" "***"))
+        (beginning-of-line 2)
+        (delete-region (point)
+                       (progn
+                         (search-forward "***END***")
+                         (beginning-of-line)
+                         (point)))
+        (dolist (baud (sort (copy-list bauds) #'<))
+          (insert (format "#ifdef B%d\n    { B%d, %d },\n#endif\n"
+                          baud baud baud)))))
+  */
+    /***BEGIN baudlist***/
+#ifdef B50
+    { B50, 50 },
+#endif
+#ifdef B75
+    { B75, 75 },
+#endif
+#ifdef B110
+    { B110, 110 },
+#endif
+#ifdef B134
+    { B134, 134 },
+#endif
+#ifdef B150
+    { B150, 150 },
+#endif
+#ifdef B200
+    { B200, 200 },
+#endif
+#ifdef B300
+    { B300, 300 },
+#endif
+#ifdef B600
+    { B600, 600 },
+#endif
+#ifdef B1200
+    { B1200, 1200 },
+#endif
+#ifdef B1800
+    { B1800, 1800 },
+#endif
+#ifdef B2400
+    { B2400, 2400 },
+#endif
+#ifdef B4800
+    { B4800, 4800 },
+#endif
+#ifdef B9600
+    { B9600, 9600 },
+#endif
+#ifdef B19200
+    { B19200, 19200 },
+#endif
+#ifdef B38400
+    { B38400, 38400 },
+#endif
+#ifdef B57600
+    { B57600, 57600 },
+#endif
+#ifdef B115200
+    { B115200, 115200 },
+#endif
+#ifdef B230400
+    { B230400, 230400 },
+#endif
+#ifdef B460800
+    { B460800, 460800 },
+#endif
+#ifdef B500000
+    { B500000, 500000 },
+#endif
+#ifdef B576000
+    { B576000, 576000 },
+#endif
+#ifdef B921600
+    { B921600, 921600 },
+#endif
+#ifdef B1000000
+    { B1000000, 1000000 },
+#endif
+#ifdef B1152000
+    { B1152000, 1152000 },
+#endif
+#ifdef B1500000
+    { B1500000, 1500000 },
+#endif
+#ifdef B2000000
+    { B2000000, 2000000 },
+#endif
+#ifdef B2500000
+    { B2500000, 2500000 },
+#endif
+#ifdef B3000000
+    { B3000000, 3000000 },
+#endif
+#ifdef B3500000
+    { B3500000, 3500000 },
+#endif
+#ifdef B4000000
+    { B4000000, 4000000 },
+#endif
+    /***END***/
+    { 0, 0 }
+  };
+
+  const struct baudtab *b;
+  const char *p; int n;
+  struct termios c;
+  speed_t code;
+
+  tty->fpout = fp;
+  if (!fp || tcgetattr(fileno(fp), &c))
+    tty->baud = 0;
+  else {
+    code = cfgetospeed(&c);
+    for (b = baudtab; b->baud; b++)
+      if (b->code == code) { tty->baud = b->baud; goto found_baud; }
+    tty->baud = 0;
+  found_baud:
+    tty_resized(tty);
+  }
+
+  if (!tty->wd)
+    { p = getenv("COLUMNS"); if (p) { n = atoi(p); if (n) tty->wd = n; } }
+  if (!tty->ht)
+    { p = getenv("LINES");   if (p) { n = atoi(p); if (n) tty->ht = n; } }
+}
+
+static void env_colour_caps(unsigned *caps_inout)
 {
-  unsigned r, g, b, y, t, range;
-  uint32 best_colour, best_space; int best_error;
+  const char *p;
+  unsigned caps = *caps_inout;
 
-#define COMMON_SCALE 3825
+  p = getenv("FORCE_COLOR");
+  if (p) switch (*p) {
+    case '0':
+      caps &= TTACF_CSPCMASK | TTACF_FG | TTACF_BG;
+      break;
+    case '1':
+      caps = (caps&~TTACF_CSPCMASK) | TTACF_FG | TTACF_BG |
+            TTACF_1BPC | TTACF_1BPCBR;
+      break;
+    case '2':
+      caps = (caps&~TTACF_CSPCMASK) | TTACF_FG | TTACF_BG |
+            TTACF_1BPC | TTACF_1BPCBR | TTACF_6LPC | TTACF_8LGS;
+      break;
+    case '3':
+      caps = (caps&~TTACF_CSPCMASK) | TTACF_FG | TTACF_BG |
+            TTACF_1BPC | TTACF_1BPCBR | TTACF_8BPC;
+      break;
+  }
+  *caps_inout = caps;
+}
 
-#define CHECK_RANGE(r) do {                                            \
-  STATIC_ASSERT(COMMON_SCALE%(r) == 0, "common scale doesn't cover " #r); \
-} while (0)
+/* --- @clamp_colours@ --- *
+ *
+ * Arguments:  @uint32 *space_out, *colour_out@ = selected space and colour
+ *             @uint32 space, colour@ = requested space and colour
+ *             @uint32 acaps@ = terminal's attribute capability mask
+ *
+ * Returns:    ---
+ *
+ * Use:                Select the best approximation to the requested colour which
+ *             can be accommodated by the terminal.
+ */
+
+/* #define DEBUG_CLAMP */
+
+static void clamp_colours(uint32 *space_out, uint32 *colour_out,
+                         uint32 space, uint32 colour, uint32 acaps)
+{
+  unsigned r, g, b, rr, gg, bb, y, t, u;
+  uint32 best_colour = 0, best_space; int best_error;
 
-#define SET_RANGE(r) do { CHECK_RANGE(r); range = (r); } while (0)
+#ifdef DEBUG_CLAMP
+#  define D(x) x
+#else
+#  define D(x)
+#endif
 
   /* Check the colour space.  If it's one that the terminal can handle, then
    * return the colour unchanged.  Otherwise, extract the channel components
    * for the next step.
    */
   switch (space) {
+
     case TTCSPC_NONE:
-      *space_out = TTCSPC_NONE; *colour_out = 0; return;
+      /* No colour wanted at all.  There's nothing to do here. */
+
+      *space_out = TTCSPC_NONE; *colour_out = 0;
+      return;
+
     case TTCSPC_1BPC:
+      /* One-bit-per-channel colour.
+       *
+       * There's no standardized mapping for these; indeed, they're commonly
+       * configurable by users.  Since there are also `bright' versions of
+       * each, it's probably not right to just map zero to zero and one to
+       * full-scale.
+       */
+
       if (colour >= 8) goto inval;
-      if (acaps&TTACF_1BPC)
-       { *space_out = TTACF_1BPC; *colour_out = colour; return; }
-      colour = basic_colour_map[colour];
-      r = TTCOL_8BR(colour); g = TTCOL_8BG(colour); b = TTCOL_8BB(colour);
-      SET_RANGE(255);
+      if (acaps&(TTACF_1BPC | TTACF_1BPCBR))
+       { *space_out = TTCSPC_1BPC; *colour_out = colour; return; }
+
+#define C1BPC_FULL 0xcc
+#define C1BPC_CVT(col, bit)                                            \
+       (C1BPC_FULL&((((col)&(1 << (bit))) << (8 - (bit))) - 1))
+      r = C1BPC_CVT(colour, 0);
+      g = C1BPC_CVT(colour, 1);
+      b = C1BPC_CVT(colour, 2);
       break;
+
     case TTCSPC_1BPCBR:
-      if (colour >= 8) goto inval;
-      if (acaps&TTACF_1BPCBR)
-       { *space_out = TTACF_1BPCBR; *colour_out = colour; return; }
-      colour = bright_colour_map[colour];
-      r = TTCOL_8BR(colour); g = TTCOL_8BG(colour); b = TTCOL_8BB(colour);
-      SET_RANGE(255);
+      /* One-bit-per-channel colour, with global brightness.  Again, there's
+       * no standardized mapping.  Apply a boost across all three channels.
+       */
+
+      if (colour >= 16) goto inval;
+      if (!(colour&TT1BPC_BRI) && (acaps&(TTACF_1BPC | TTACF_1BPCBR)))
+       { *space_out = TTCSPC_1BPC; *colour_out = colour; return; }
+      else if (acaps&TTACF_1BPCBR)
+       { *space_out = TTCSPC_1BPCBR; *colour_out = colour; return; }
+
+#define C1BPC_BRIGHT 0x33
+      r = C1BPC_CVT(colour, 0) + C1BPC_BRIGHT;
+      g = C1BPC_CVT(colour, 1) + C1BPC_BRIGHT;
+      b = C1BPC_CVT(colour, 2) + C1BPC_BRIGHT;
+#undef C1BPC_CVT
       break;
-    case TTCSPC_2BPC:
+
+    case TTCSPC_4LPC:
+      /* Four-levels-per-channel colour.  These are part of an `indexed'
+       * colour space which is theoretically controlled by applications but
+       * (a) that's rare, and (b) worrying about that won't do us any good.
+       *
+       * Each channel has four levels, but they're not assigned linearly.
+       */
+
       if (colour >= 64) goto inval;
-      if (acaps&TTACF_2BPC)
-       { *space_out = TTACF_2BPC; *colour_out = colour; return; }
-      r = TTCOL_2BR(colour); g = TTCOL_2BG(colour); b = TTCOL_2BB(colour);
-      SET_RANGE(3);
+      if (acaps&TTACF_4LPC)
+       { *space_out = TTCSPC_4LPC; *colour_out = colour; return; }
+
+#define C4LPC_L0 0
+#define C4LPC_L1 139
+#define C4LPC_L2 205
+#define C4LPC_L3 255
+#define C4LPC_CVT(ch) (t = (ch), t == 0 ? C4LPC_L0 :                   \
+                                t == 1 ? C4LPC_L1 :                    \
+                                t == 2 ? C4LPC_L2 :                    \
+                                         C4LPC_L3)
+      r = C4LPC_CVT(TTCOL_2BR(colour));
+      g = C4LPC_CVT(TTCOL_2BG(colour));
+      b = C4LPC_CVT(TTCOL_2BB(colour));
+#undef C4LPC_CVT
       break;
+
     case TTCSPC_8LGS:
+      /* Eight-levels greyscale.  Again, these are part of an `indexed'
+       * colour space which is under application control, and, again, the
+       * levels aren't linear, but they're not far off linear.
+       */
       if (colour >= 8) goto inval;
       if (acaps&TTACF_8LGS)
-       { *space_out = TTACF_8LGS; *colour_out = colour; return; }
-      r = g = b = colour + 1;
-      SET_RANGE(9);
+       { *space_out = TTCSPC_8LGS; *colour_out = colour; return; }
+
+      r = g = b = 255*(colour ? colour + 3 : 2)/11;
       break;
+
     case TTCSPC_6LPC:
+      /* Six-levels-per-channel colour.  Again, `indexed' colour space under
+       * application control.  This time the mapping is essentially liner.
+       */
+
       if (colour >= 216) goto inval;
       if (acaps&TTACF_6LPC)
-       { *space_out = TTACF_6LPC; *colour_out = colour; return; }
-      r = TTCOL_6LR(colour); g = TTCOL_6LG(colour); b = TTCOL_6LB(colour);
-      SET_RANGE(5);
+       { *space_out = TTCSPC_6LPC; *colour_out = colour; return; }
+
+#define C6LPC_CVT(ch) (t = (ch), t ? (40*t + 55) : 0)
+      r = C6LPC_CVT(TTCOL_6LR(colour));
+      g = C6LPC_CVT(TTCOL_6LG(colour));
+      b = C6LPC_CVT(TTCOL_6LB(colour));
+#undef C6LPC_CVT
       break;
+
     case TTCSPC_24LGS:
+      /* Twenty-four-levels greyscale.  Same story. */
+
       if (colour >= 24) goto inval;
       if (acaps&TTACF_24LGS)
-       { *space_out = TTACF_24LGS; *colour_out = colour; return; }
-      r = g = b = colour + 1;
-      SET_RANGE(25);
+       { *space_out = TTCSPC_24LGS; *colour_out = colour; return; }
+
+      r = g = b = 10*colour + 8;
       break;
+
     case TTCSPC_8BPC:
+      /* Eight-bits-per-channel colour.  No conversion to apply here. */
+
       if (colour >= 0x01000000) goto inval;
       if (acaps&TTACF_8BPC)
-       { *space_out = TTACF_8BPC; *colour_out = colour; return; }
+       { *space_out = TTCSPC_8BPC; *colour_out = colour; return; }
+
       r = TTCOL_8BR(colour); g = TTCOL_8BG(colour); b = TTCOL_8BB(colour);
-      SET_RANGE(255);
       break;
-      default:
-    goto inval;
-  }
 
-#undef SET_RANGE
+    default:
+      /* Anything else. */
+
+      goto inval;
+  }
 
   /* We didn't get an exact match, so we'll have to make do with what we've
    * got.
    */
-  best_error = -1;
+  best_error = -1; best_space = TTCSPC_NONE; best_colour = 0;
+  D( fprintf(stderr, "\n;; APPROX space %u, colour 0x%lx = %u/%u/%u\n",
+            space, (unsigned long)colour, r, g, b); )
 
+  /* Approximate colour weightings for human colour vision. */
 #define RWT 2
 #define GWT 4
 #define BWT 1
+#define TOTWT (RWT + GWT + BWT)
 
-  t = COMMON_SCALE/range; r *= t; g *= t; b *= t;
-  y = (RWT*r + GWT*g + BWT*b + (RWT + GWT + BWT)/2)/(RWT + GWT + BWT);
+  /* Determine the optimal grey approximation for this colour. */
+  y = (RWT*r + GWT*g + BWT*b + TOTWT/2)/TOTWT;
 
-#define TRY_APPROX(red, grn, blu, spc, clr) do {                       \
-  int r_err = (red) - r, g_err = (grn) - g, b_err = (blu) - b;         \
-  int err;                                                             \
+#define TRY_APPROX(rr, gg, bb, spc, clr) do {                          \
+  /* If the approximation (RR, GG, BB) is closer to the current best   \
+   * then accept SPC and CLR as the new best space/colour option.      \
+   */                                                                  \
+                                                                       \
+  int _r_err = (rr) - r, _g_err = (gg) - g, _b_err = (bb) - b;         \
+  int _err;                                                            \
                                                                        \
-  if (r_err < 0) r_err = -r_err;                                       \
-  if (g_err < 0) g_err = -g_err;                                       \
-  if (b_err < 0) b_err = -b_err;                                       \
+  if (_r_err < 0) _r_err = -_r_err;                                    \
+  if (_g_err < 0) _g_err = -_g_err;                                    \
+  if (_b_err < 0) _b_err = -_b_err;                                    \
                                                                        \
-  err = r_err + g_err + b_err;                                         \
-  if (best_error < 0 || err < best_error)                              \
-    { best_error = err; best_space = (spc); best_colour = (clr); }     \
+  _err = RWT*_r_err + GWT*_g_err + BWT*_b_err;                         \
+  D( fprintf(stderr,                                                   \
+            ";;   candidate space %u, colour 0x%lx = %u/%u/%u; "       \
+                 "error = %d\n",                                       \
+            (spc), (unsigned long)(clr), (rr), (gg), (bb), _err); )    \
+  if (best_error < 0 || _err < best_error) {                           \
+    best_error = _err; best_space = (spc); best_colour = (clr);                \
+    D( fprintf(stderr, ";;\tNEW BEST APPROXIMATION\n"); )              \
+  }                                                                    \
+} while (0)
+
+  if (!(acaps&(TTACF_4LPC | TTACF_6LPC | TTACF_8BPC))) {
+    /* The one-bit-per-channel colours are very variable, but there's little
+     * choice, so we'll have to try.  We assume the same mapping as on the
+     * way in.
+     */
+
+    if (acaps&(TTACF_1BPC | TTACF_1BPCBR)) {
+      /* One-bit-per-channel colour. */
+
+#define C1BPC_APPROX(cc, c, bit) do {                                  \
+      if ((c) <= C1BPC_FULL/2) (cc) = 0;                               \
+      else { (cc) = C1BPC_FULL; t |= (bit); }                          \
+} while (0)
+      t = 0;
+      C1BPC_APPROX(rr, r, TT1BPC_RED);
+      C1BPC_APPROX(gg, g, TT1BPC_GRN);
+      C1BPC_APPROX(bb, b, TT1BPC_BLU);
+#undef C1BPC_APPROX
+      TRY_APPROX(rr, gg, bb, TTCSPC_1BPC, t);
+    }
+
+    if (acaps&TTACF_1BPCBR) {
+      /* One-bit-per-channel colour, with global brightness. */
+
+#define C1BPCBR_APPROX(cc, c, bit) do {                                        \
+      if ((c) <= C1BPC_FULL/2 + C1BPC_BRIGHT) (cc) = C1BPC_BRIGHT;     \
+      else { (cc) = 255; t |= (bit); }                                 \
 } while (0)
+      t = TT1BPC_BRI;
+      C1BPCBR_APPROX(rr, r, TT1BPC_RED);
+      C1BPCBR_APPROX(gg, g, TT1BPC_GRN);
+      C1BPCBR_APPROX(bb, b, TT1BPC_BLU);
+#undef C1BPCBR_APPROX
+      TRY_APPROX(rr, gg, bb, TTCSPC_1BPCBR, t);
+    }
+  }
 
-#define TRY_RGB(spc, lvls) do {                                                \
-  CHECK_RANGE((lvls) - 1);                                             \
-  unsigned sc = COMMON_SCALE/((lvls) - 1);                             \
-  unsigned rr, gg, bb, clr;                                            \
+  if (acaps&TTACF_4LPC) {
+    /* Four-levels-per-channel colour. */
+
+#define C4LPC_APPROX(cc, c, sh) do {                                   \
+    unsigned _c = (c);                                                 \
                                                                        \
-  rr = (r + sc/2)/sc; gg = (g + sc/2)/sc; bb = (b + sc/2)/sc;          \
-  TRY_APPROX(rr*sc, gg*sc, bb*sc, (spc), (rr*(lvls) + bb)*(lvls) + gg); \
+    if (_c > (C4LPC_L2 + C4LPC_L3)/2)                                  \
+      { (cc) = C4LPC_L3; t |= 3 << (sh); }                             \
+    else if (_c > (C4LPC_L1 + C4LPC_L2)/2)                             \
+      { (cc) = C4LPC_L2; t |= 2 << (sh); }                             \
+    else if (_c > (C4LPC_L0 + C4LPC_L1)/2)                             \
+      { (cc) = C4LPC_L1; t |= 1 << (sh); }                             \
+    else                                                               \
+      (cc) = C4LPC_L0;                                                 \
 } while (0)
+    t = 0;
+    C4LPC_APPROX(rr, r, 4);
+    C4LPC_APPROX(gg, g, 2);
+    C4LPC_APPROX(bb, b, 0);
+#undef C4LPC_APPROX
+    TRY_APPROX(rr, gg, bb, TTCSPC_4LPC, t);
+  }
+
+  if (acaps&TTACF_8LGS) {
+    /* Eight-levels greyscale. */
+
+    u = (11*y)/255;
+    if (u <= 2) { u = 2; t = 0; }
+    else if (u == 3) { u = 4; t = 1; }
+    else if (u == 11) { u = 10; t = 7; }
+    else t = u - 3;
+    u = (255*u)/11; TRY_APPROX(u, u, u, TTCSPC_8LGS, t);
+  }
+
+  if (acaps&TTACF_6LPC) {
+    /* Six-levels-per-channel colour. */
 
-#define TRY_GREY(spc, lvls) do {                                       \
-  CHECK_RANGE((lvls) + 1);                                             \
-  unsigned sc = COMMON_SCALE/((lvls) + 1);                             \
-  unsigned yy, grey;                                                   \
+#define C6LPC_APPROX(cc, c, f) do {                                    \
+    unsigned _c = (c);                                                 \
                                                                        \
-  yy = (y + sc/2)/sc;                                                  \
-  if (yy >= 1 && yy <= (lvls))                                         \
-    { grey = yy*sc; TRY_APPROX(grey, grey, grey, (spc), grey - 1); }   \
+    if (_c < 36) (cc) = 0;                                             \
+    else { u = (_c - 36)/40; t += (f)*u; (cc) = 40*u + 55; }           \
 } while (0)
+    t = 0;
+    C6LPC_APPROX(rr, r, 36);
+    C6LPC_APPROX(gg, g,  6);
+    C6LPC_APPROX(bb, b,  1);
+#undef C6LPC_APPROX
+    TRY_APPROX(rr, gg, bb, TTCSPC_6LPC, t);
+  }
 
-  if (acaps&TTACF_1BPC)
-    for (i = 0; i < 8; i++) {
-      t = basic_colour_map[i];
-      TRY_APPROX(TTCOL_8BR(t), TTCOL_8BG(t), TTCOL_8BB(t), TTCSPC_1BPC, i);
-    }
-  if (acaps&TTACF_1BPCBR)
-    for (i = 0; i < 8; i++) {
-      t = bright_colour_map[i];
-      TRY_APPROX(TTCOL_8BR(t), TTCOL_8BG(t), TTCOL_8BB(t), TTCSPC_1BPCBR, i);
+  if (acaps&TTACF_24LGS) {
+    /* Twenty-four-levels greyscale. */
+
+    if (y < 3) { t = 0; u = 8; }
+    else if (y >= 243) { t = 23; u = 238; }
+    else { t = (y - 3)/10; u = 10*t + 8; }
+    TRY_APPROX(u, u, u, TTCSPC_24LGS, t);
+  }
+
+  if (acaps&TTACF_8BPC) {
+    /* Eight-bits-per-channel colour. */
+
+    if (best_error) {
+      D( fprintf(stderr, ";;   accept exact 8bpc colour\n"); )
+      best_error = 0; best_space = TTCSPC_8BPC;
+      best_colour = TTCOL_MK8B(r, g, b);
     }
-  if (acaps&TTACF_2BPC) TRY_RGB(TTCSPC_2BPC, 4);
-  if (acaps&TTACF_8LGS) TRY_GREY(TTCSPC_8LGS, 8);
-  if (acaps&TTACF_6LPC) TRY_RGB(TTCSPC_6LPC, 6);
-  if (acaps&TTACF_24LGS) TRY_GREY(TTCSPC_24LGS, 24);
-  if (acaps&TTACF_8BPC) TRY_RGB(TTCSPC_8BPC, 256);
+  }
+
+  /* Done. */
+  *space_out = best_space; *colour_out = best_colour;
+  return;
+
+inval:
+  /* Invalid colour selection.  Ignore this. */
+  *space_out = TTCSPC_NONE; *colour_out = 0;
+
+#undef C1BPC_FULL
+#undef C1BPC_BRIGHT
+
+#undef C4LPC_L0
+#undef C4LPC_L1
+#undef C4LPC_L2
+#undef C4LPC_L3
 
 #undef TRY_APPROX
 #undef TRY_RGB
@@ -220,19 +586,32 @@ static void clamp_colours(uint32 *space_out, uint32 *colour_out,
 #undef RWT
 #undef GWT
 #undef BWT
+#undef TOTWT
 
-  *space_out = best_space; *colour_out = best_colour;
-  return;
+#undef SET_RANGE
+#undef CHECK_RANGE
 
-inval:
-  *space_out = TTCSPC_NONE; *colour_out = 0;
+#undef D
 }
 
-void tty_clampattr(struct tty_attr *a_out, const struct tty_attr *a,
-                  uint32 acaps)
+/* --- @tty_clampattr@ --- *
+ *
+ * Arguments:  @struct tty_attr *a_out@ = selected attributes
+ *             @const struct tty_attr *a@ = requested attributes
+ *             @uint32 acaps@ = terminal's attribute capability mask
+ *
+ * Returns:    ---
+ *
+ * Use:                Select the closest approximation to the requested attributes
+ *             which can be accommodated by the terminal.
+ */
+
+void tty_clampattr(struct tty_attr *a_out,
+                  const struct tty_attr *a, uint32 acaps)
 {
-  uint32 ff = 0, f = a->f, t;
+  uint32 ff = 0, f = a ? a->f : 0, t;
 
+  /* Line attributes. */
   t = (f&TTAF_LNMASK) >> TTAF_LNSHIFT;
   switch (t) {
     case TTLN_NONE:
@@ -242,41 +621,54 @@ void tty_clampattr(struct tty_attr *a_out, const struct tty_attr *a,
       break;
     case TTLN_UULINE:
       if (acaps&TTACF_UULINE) ;
-      else if (acaps&TTACF_ULINE) t = TTLN_ULNE;
+      else if (acaps&TTACF_ULINE) t = TTLN_ULINE;
       else t = TTLN_NONE;
       break;
-    case TTLN_STRIKE:
-      if (!acaps&TTACF_STRIKE) t = TTLN_NONE;
-      break;
     default:
       t = TTLN_NONE;
   }
   ff |= t << TTAF_LNSHIFT;
 
+  /* Text weight. */
   t = (f&TTAF_WTMASK) >> TTAF_WTSHIFT;
   switch (t) {
     case TTWT_MED: break;
     case TTWT_BOLD: if (!(acaps&TTACF_BOLD)) t = TTWT_MED; break;
     case TTWT_DIM: if (!(acaps&TTACF_DIM)) t = TTWT_MED; break;
-    default: t = TTWD_MED; break;
+    default: t = TTWT_MED; break;
   }
   ff |= t << TTAF_WTSHIFT;
 
+  /* Other text attributes. */
+  if (acaps&TTACF_STRIKE) ff |= f&TTAF_STRIKE;
   if (acaps&TTACF_ITAL) ff |= f&TTAF_ITAL;
   if (acaps&TTACF_INVV) ff |= f&TTAF_INVV;
 
+  /* Foreground and background colours. */
   if (acaps&TTACF_FG) {
     clamp_colours(&t, &a_out->fg,
-                 (f&TTAF_FGSPCMASK) >> TTAF_FGSPCSHIFT, a->fg);
+                 (f&TTAF_FGSPCMASK) >> TTAF_FGSPCSHIFT, a ? a->fg : 0,
+                 acaps);
     ff |= t << TTAF_FGSPCSHIFT;
   }
   if (acaps&TTACF_BG) {
     clamp_colours(&t, &a_out->bg,
-                 (f&TTAF_BGSPCMASK) >> TTAF_BGSPCSHIFT, a->bg);
+                 (f&TTAF_BGSPCMASK) >> TTAF_BGSPCSHIFT, a ? a->bg : 0,
+                 acaps);
     ff |= t << TTAF_BGSPCSHIFT;
   }
 
-  a_out->f = ff; a_out->_res = 0;
+  /* All done. */
+  a_out->f = ff; a_out->_res0 = 0;
+}
+
+int tty_resized(struct tty *tty)
+{
+  struct winsize ws;
+
+  if (!tty->fpout) { errno = ENOTTY; return (-1); }
+  if (ioctl(fileno(tty->fpout), TIOCGWINSZ, &ws)) return (-1);
+  tty->wd = ws.ws_col; tty->ht = ws.ws_row; return (0);
 }
 
 /*----- Common machinery for `termcap' and `terminfo' ---------------------*/
@@ -289,66 +681,77 @@ void tty_clampattr(struct tty_attr *a_out, const struct tty_attr *a,
 
 static const struct gprintf_ops *global_gops;
 static void *global_gout;
+static struct tty *global_lock = 0;
 
-static int term_putch(int ch)
+static void caps_release(struct tty *tty)
+  { assert(global_lock == tty); global_lock = 0; }
+
+static int caps_putch(int ch)
   { return (global_gops->putch(global_gout, ch)); }
 
 #endif
 
 #ifdef HAVE_UNIBILIUM
-#  define UNIBI_(x) unibi_##x
+#  define UNIBI_(x) unibi##x
 #else
 #  define UNIBI_(x) 0
 #endif
 
 #define BASICCAPS(_bool, _int, _str)                                   \
-  _str(carriage_return, cr, cr)                                                \
-  _int(lines, lines, li) _int(columns, cols, co)
+  _str(_repeat_char, rep, rp)                                          \
+  _int(_padding_baud_rate, pb, pb) _str(_pad_char, pad, pc)            \
+  _bool(_no_pad_char, npc, NP) _bool(_xon_xoff, xon, xo)               \
+  _bool(_move_insert_mode, mir, mi) _bool(_move_standout_mode, msgr, ms)
 
 #define ATTRCAPS(_bool, _int, _str)                                    \
-  _str(exit_attribute_mode, sgr0, me)                                  \
-  _str(enter_underline_mode, smul, us) _str(exit_underline_mode, rmul, ue) \
-  _str(enter_italics_mode, sitm, ZH) _str(exit_italics_mode, ritm, ZR) \
-  _str(enter_bold_mode, bold, md) _str(enter_dim_mode, dim, mh)                \
-  _str(enter_reverse_mode, rev, mr)                                    \
+  _str(_exit_attribute_mode, sgr0, me)                                 \
+  _str(_enter_underline_mode, smul, us)                                        \
+    _str(_exit_underline_mode, rmul, ue)                               \
+  _str(_enter_italics_mode, sitm, ZH) _str(_exit_italics_mode, ritm, ZR) \
+  _str(_enter_bold_mode, bold, md) _str(_enter_dim_mode, dim, mh)      \
+  _str(_enter_reverse_mode, rev, mr)                                   \
   COLOURCAPS(_bool, _int, _str)
 
 #define COLOURCAPS(_bool, _int, _str)                                  \
-  _str(set_a_foreground, setaf, AF) _str(set_a_background, setab, AB)  \
-  _str(orig_pair, op, op) _int(max_colors, colors, Co)
+  _str(_set_a_foreground, setaf, AF) _str(_set_a_background, setab, AB)        \
+  _str(_orig_pair, op, op) _int(_max_colors, colors, Co)
 
 #define MODECAPS(_bool, _int, _str)                                    \
-  _str(enter_am_mode, smam, SA) _str(exit_am_mode, rmam, RA)           \
-  _str(enter_ca_mode, smcup, ti) _str(exit_ca_mode, rmcup, te)         \
-  _str(enter_insert_mode, smir, im) _str(exit_insert_mode, rmir, ei)
+  _str(_carriage_return, cr, cr) _str(_newline, nel, nw)               \
+  _str(_enter_am_mode, smam, SA) _str(_exit_am_mode, rmam, RA)         \
+  _str(_enter_ca_mode, smcup, ti) _str(_exit_ca_mode, rmcup, te)       \
+  _str(_cursor_normal, cnorm, vs) _str(_cursor_invisible, civis, vi)   \
+  _str(_enter_insert_mode, smir, im) _str(_exit_insert_mode, rmir, ei) \
+  _str(_enter_delete_mode, smdc, dm) _str(_exit_delete_mode, rmdc, ed)
 
 #define MOVECAPS(_bool, _int, _str)                                    \
-  _str(carriage_return, cr, cr)                                                \
-  _str(cursor_home, home, ho)                                          \
-  _str(cusor_address, cup, cm)                                         \
-  _str(row_address, vpa, cv) _str(column_address, hpa, ch)             \
-  _str(cursor_left, cub1, le) _(parm_left_cursor, cub, LE)             \
-  _str(cursor_right, cuf1, nd) _(parm_right_cursor, cuf, RI)           \
-  _str(cursor_up, cuu1, up) _str(parm_up_cursor, cuu, UP)              \
-  _str(cursor_down, cud1, do) _str(parm_down_cursor, cud, DO)
+  _str(_cursor_home, home, ho)                                         \
+  _str(_cursor_address, cup, cm)                                       \
+  _str(_row_address, vpa, cv) _str(_column_address, hpa, ch)           \
+  _str(_cursor_left, cub1, le) _str(_parm_left_cursor, cub, LE)                \
+  _str(_cursor_right, cuf1, nd) _str(_parm_right_cursor, cuf, RI)      \
+  _str(_cursor_up, cuu1, up) _str(_parm_up_cursor, cuu, UP)            \
+  _str(_cursor_down, cud1, do) _str(_parm_down_cursor, cud, DO)
 
 #define SCROLLCAPS(_bool, _int, _str)                                  \
-  _str(change_scroll_region, csr, cs)                                  \
-  _str(scroll_forward, ind, sf) _str(parm_index, indn, SF)             \
-  _str(scroll_reverse, ri, sr) _str(parm_rindex, rin, SR)
+  _str(_change_scroll_region, csr, cs)                                 \
+  _str(_scroll_forward, ind, sf) _str(_parm_index, indn, SF)           \
+  _str(_scroll_reverse, ri, sr) _str(_parm_rindex, rin, SR)
 
 #define ERASECAPS(_bool, _int, _str)                                   \
-  _str(clr_bol, el1, cb)                                               \
-  _str(clr_eol, el, ce)                                                        \
-  _str(clr_eos, ed, cd)
+  _str(_erase_chars, ech, ec)                                          \
+  _str(_clr_bol, el1, cb) _str(_clr_eol, el, ce)                       \
+  _str(_clr_eos, ed, cd) _str(_clear_screen, clear, cl)
 
 #define INSDELCAPS(_bool, _int, _str)                                  \
-  _str(insert_character, ich1, ic) _str(parm_ich, ich, IC)             \
-  _str(insert_line, il1, al) _str(parm_insert_line, il, AL)            \
-  _str(delete_character, dch1, dc) _str(parm_dch, dch, DC)             \
-  _str(delete_line, dl1, dl) _str(parm_delete_line, dl, DL)
+  _str(_insert_character, ich1, ic) _str(_parm_ich, ich, IC)           \
+  _str(_insert_padding, ip, ip)                                                \
+  _str(_insert_line, il1, al) _str(_parm_insert_line, il, AL)          \
+  _str(_delete_character, dch1, dc) _str(_parm_dch, dch, DC)           \
+  _str(_delete_line, dl1, dl) _str(_parm_delete_line, dl, DL)
 
 #define STORECAPS(_bool, _int, _str)                                   \
+  BASICCAPS(_bool, _int, _str)                                         \
   ATTRCAPS(_bool, _int, _str)                                          \
   MODECAPS(_bool, _int, _str)                                          \
   MOVECAPS(_bool, _int, _str)                                          \
@@ -356,159 +759,1836 @@ static int term_putch(int ch)
   ERASECAPS(_bool, _int, _str)                                         \
   INSDELCAPS(_bool, _int, _str)
 
-#define CAP_XMC magic_cookie_glitch, xmc, sg
-#define CAP_BCE back_color_erase, bce, ut
-#define CAP_XHPA row_addr_glitch, xvpa, YD
-#define CAP_XVPA col_addr_glitch, xhpa, YA
-#define CAP_MIR move_insert_mode, mir, mi
-#define CAP_MSGR move_standout_mode, msgr, ms
-#define CAP_NPC no_pad_char, npc, NP
-#define CAP_AM auto_left_margin, am, am
-#define CAP_XENL eat_newline_glitch, xenl, xn
-
 #define CAPREF(var, info, cap) UNIBI_(var), #info, #cap
 
-struct ttycaps {
-#define DEF_STRCAP(info, cap) const char *cap;
-#define DEF_INTCAP(info, cap) int cap;
-  STORECAPS(DEF_STRCAP, DEF_INTCAP)
-#undef DEF_STRCAP
+#define CAP_XMC CAPREF(_magic_cookie_glitch, xmc, sg)
+#define CAP_BCE CAPREF(_back_color_erase, bce, ut)
+#define CAP_XHPA CAPREF(_row_addr_glitch, xvpa, YD)
+#define CAP_XVPA CAPREF(_col_addr_glitch, xhpa, YA)
+#define CAP_AM CAPREF(_auto_right_margin, am, am)
+#define CAP_XENL CAPREF(_eat_newline_glitch, xenl, xn)
+#define CAP_HT CAPREF(_lines, lines, li)
+#define CAP_WD CAPREF(_columns, cols, co)
+
+#define TTY_BASEOPSPFX struct tty_ops tty
+#define TTY_BASEOPSUXFX struct tty_ops tty
+#define TTY_BASEPFX struct tty tty
+#define TTY_BASEUSFX struct tty tty
+
+struct tty_capopslots {
+  int (*boolcap)(struct tty */*tty*/,
+                int /*uix*/, const char */*info*/, const char */*cap*/);
+  int (*intcap)(struct tty */*tty*/,
+               int /*uix*/, const char */*info*/, const char */*cap*/);
+  const char *(*strcap)(struct tty */*tty*/,
+                       int /*uix*/, const char */*info*/,
+                       const char */*cap*/);
+  int (*put0)(struct tty */*tty*/,
+             const struct gprintf_ops */*gops*/, void */*go*/,
+             unsigned /*npad*/, const char */*cap*/);
+  int (*put1i)(struct tty */*tty*/,
+              const struct gprintf_ops */*gops*/, void */*go*/,
+              unsigned /*npad*/, const char */*cap*/, int /*i0*/);
+  int (*put2i)(struct tty */*tty*/,
+              const struct gprintf_ops */*gops*/, void */*go*/,
+              unsigned /*npad*/,
+              const char */*cap*/, int /*i0*/, int /*i1*/);
+};
+#define TTY_CAPOPSPFX TTY_BASEOPSPFX; struct tty_capopslots cap
+struct tty_capops { TTY_CAPOPSPFX; };
+#define TTY_CAPOPSUSFX struct tty_capops cap; TTY_BASEOPSUXFX
+union tty_capopsu { TTY_CAPOPSUSFX; };
+
+struct tty_capslots {
+#define DEF_BOOLCAP(uix, info, cap) unsigned info : 1;
+#define DEF_INTCAP(uix, info, cap) int info;
+#define DEF_STRCAP(uix, info, cap) const char *info;
+  STORECAPS(DEF_BOOLCAP, DEF_INTCAP, DEF_STRCAP)
+#undef DEF_BOOLCAP
 #undef DEF_INTCAP
+#undef DEF_STRCAP
 };
-
-typedef int boolcapfn(int uix, const char *info, const char *cap, void *arg);
-typedef int intcapfn(int uix, const char *info, const char *cap, void *arg);
-typedef const char *strcapfn(int uix, const char *info, const char *cap,
-                            void *arg);
-
-#define DEFINE_CAPISH(PRE, pre)
-
-static int init_caps(struct tty *tty, struct tty_caps *caps,
-                    boolcapfn *boolcap, intcapfn *intcap, strcapfn *strcap,
-                    void *arg)
+#define TTY_CAPSPFX                                                    \
+  struct tty tty;                                                      \
+  struct tty_capslots cap
+struct tty_caps { TTY_CAPSPFX; };
+#define TTY_CAPSUSFX                                                   \
+  struct tty_caps cap;                                                 \
+  struct tty tty
+union tty_capsu { TTY_CAPSUSFX; };
+
+static void init_caps(struct tty_caps *t)
 {
-  tty->acaps = tty->ocaps = 0;
-  tty->st.modes = 0;
-  tty->st.attr.f = 0;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int wd, ht;
+
+  t->tty.acaps = t->tty.ocaps = 0;
+  t->tty.st.modes = 0; t->tty.st.attr.f = 0;
+  t->tty.st.attr.fg = t->tty.st.attr.bg = 0;
 
   /* Inhale all of the interesting terminal capabilities. */
-#define GETBOOL(var, info, cap)                                                \
-       caps->info = boolcap(CAPREF(var, info, cap), arg);
-#define GETINT(var, info, cap)                                         \
-       caps->info = intcap(CAPREF(var, info, cap), arg);
-#define GETSTR(var, info, cap)                                         \
-       caps->info = strcap(CAPREF(var, info, cap), arg);
+#define GETBOOL(var, info, cap_)                                       \
+    t->cap.info = ops->cap.boolcap(&t->tty, CAPREF(var, info, cap_));
+#define GETINT(var, info, cap_)                                                \
+    t->cap.info = ops->cap.intcap(&t->tty, CAPREF(var, info, cap_));
+#define GETSTR(var, info, cap_)                                                \
+    t->cap.info = ops->cap.strcap(&t->tty, CAPREF(var, info, cap_));
   STORECAPS(GETBOOL, GETINT, GETSTR)
 #undef GETBOOL
 #undef GETINT
 #undef GETSTR
 
-#define CLEARCAP(var, info, cap) caps->info = 0;
-#define CLEAR_CAPS(caplist) do { caplist(CLEARCAP) } while (0)
+#define CLEAR_BOOL(uix, info, cap_) t->cap.info = 0;
+#define CLEAR_INT(uix, info, cap_) t->cap.info = 0;
+#define CLEAR_STR(uix, info, cap_) t->cap.info = 0;
+#define CLEAR_CAPS(caplist)                                            \
+    do { caplist(CLEAR_BOOL, CLEAR_INT, CLEAR_STR) } while (0)
 
   /* Basic capabilities. */
-  if (!caps->cr) caps->cr = "\r";
+  if (!t->cap.cr) t->cap.cr = "\r";
+  if (!t->cap.nel) t->cap.nel = "\r\n";
 
   /* Attribute capabilities. */
-  if (intcap(CAPREF(CAP_XMC)) || !caps->sgr0)
+  if (ops->cap.intcap(&t->tty, CAP_XMC) > 0 || !t->cap.sgr0)
     CLEAR_CAPS(ATTRCAPS);
   else {
-    if (caps->smul) tty->acaps |= TTACF_ULINE;
-    if (caps->bold) tty->acaps |= TTACF_BOLD;
-    if (caps->dim) tty->acaps |= TTACF_DIM;
-    if (caps->sitm) tty->acaps |= TTACF_ITAL;
-    if (caps->rev) tty->acaps |= TTACF_INVV;
+    if (t->cap.smul) t->tty.acaps |= TTACF_ULINE;
+    if (t->cap.bold) t->tty.acaps |= TTACF_BOLD;
+    if (t->cap.dim) t->tty.acaps |= TTACF_DIM;
+    if (t->cap.sitm) t->tty.acaps |= TTACF_ITAL;
+    if (t->cap.rev) t->tty.acaps |= TTACF_INVV;
 
-    if (!caps->colors >= 8 || (!caps->setaf && !caps->setbf))
+    if (!t->cap.op || (!t->cap.setaf && !t->cap.setab))
       CLEAR_CAPS(COLOURCAPS);
     else {
-      if (caps->setaf) tty->acaps |= TTACF_FG;
-      if (caps->setab) tty->acaps |= TTACF_BG;
-      tty->acaps |= TTACF_1BPC;
-      if (caps->colors >= 16) tty->acaps |= TTACF_1BPCBR;
-      if (caps->colors == 88) tty->acaps |= TTACF_2BPC | TTACF_8LGS;
-      else if (caps->colors >= 256) tty->acaps |= TTACF_6LPC | TTACF_24LGS;
-      if (caps->colors >= 16777216) tty->acaps |= TTACF_8BPC;
-      if (boolcap(CAPREF(CAP_BCE))) tty->ocaps |= TTCF_BGER;
+      if (t->cap.setaf) t->tty.acaps |= TTACF_FG;
+      if (t->cap.setab) t->tty.acaps |= TTACF_BG;
+      t->tty.acaps |= TTACF_1BPC;
+      if (t->cap.colors >= 16777216)
+       t->tty.acaps |= TTACF_1BPC | TTACF_8BPC;
+      else if (t->cap.colors >= 256)
+       t->tty.acaps |= TTACF_1BPCBR | TTACF_6LPC | TTACF_24LGS;
+      else if (t->cap.colors == 88)
+       t->tty.acaps |= TTACF_1BPCBR | TTACF_4LPC | TTACF_8LGS;
+      else if (t->cap.colors >= 16)
+       t->tty.acaps |= TTACF_1BPCBR;
+      if (ops->cap.boolcap(&t->tty, CAP_BCE)) t->tty.ocaps |= TTCF_BGER;
+      env_colour_caps(&t->tty.acaps);
     }
   }
 
   /* Motion capabilities. */
-  if (boolcap(CAPREF(CAP_XVPA))) caps->vpa = 0;
-  if (boolcap(CAPREF(CAP_XHPA))) caps->hpa = 0;
-  if (!caps->cub1) caps->cub1 = "\b";
-  if (!caps->cud1) caps->cud1 = "\n";
-  if ((caps->cuf || caps->cuf1) && (caps->cuu || caps->cuu1)) {
-    tty->ocaps |= TTCF_RELMV;
-    if (caps->vpa) tty->ocaps |= TTCF_MIXMV;
-  }
-  if (caps->cup || (caps->hpa && caps->vpa)) tty->ocaps |= TTCF_ABSMV;
+  if (ops->cap.boolcap(&t->tty, CAP_XHPA)) t->cap.hpa = 0;
+  if (ops->cap.boolcap(&t->tty, CAP_XVPA)) t->cap.vpa = 0;
+  if (!t->cap.cub1) t->cap.cub1 = "\b";
+  if ((t->cap.cuf || t->cap.cuf1) &&
+      (t->cap.cuu || t->cap.cuu1) &&
+      (t->cap.cud || t->cap.cud1)) {
+    t->tty.ocaps |= TTCF_RELMV;
+    if (t->cap.vpa) t->tty.ocaps |= TTCF_ABSMV | TTCF_MIXMV;
+    else if (t->cap.home) t->tty.ocaps |= TTCF_ABSMV;
+  } else if (t->cap.cup ||
+            (t->cap.hpa && t->cap.vpa) ||
+            (t->cap.home && (t->cap.cuf || t->cap.cuf1)))
+    t->tty.ocaps |= TTCF_ABSMV;
 
   /* Mode capabilities. */
-  if (caps->smam && caps->rmam) tty->ocaps |= TTMF_AUTOM;
-  if (caps->smir && caps->rmir) tty->ocaps |= TTMF_INS;
-  if (boolcap(CAPREF(CAP_AM))) {
-    tty->st.modes |= TTMF_AUTOM;
-    if (boolcap(CAPREF(CAP_XENL))) tty->ocaps |= TTCF_MMARG;
+  if (t->cap.smam && t->cap.rmam) t->tty.ocaps |= TTMF_AUTOM;
+  if (t->cap.smcup && t->cap.rmcup) t->tty.ocaps |= TTMF_FSCRN;
+  if (t->cap.smir && t->cap.rmir) t->tty.ocaps |= TTMF_INS;
+  if (t->cap.smdc && t->cap.rmdc) t->tty.ocaps |= TTMF_DEL;
+  if (t->cap.cnorm && t->cap.civis)
+    { t->tty.ocaps |= TTMF_CVIS; t->tty.st.modes |= TTMF_CVIS; }
+  if (ops->cap.boolcap(&t->tty, CAP_AM)) {
+    t->tty.st.modes |= TTMF_AUTOM;
+    if (ops->cap.boolcap(&t->tty, CAP_XENL)) t->tty.ocaps |= TTCF_MMARG;
   }
 
-  /* Scrolling. */
-  if (caps->csr) tty->ocaps |= TTCF_SCRGN;
-  if ((caps->ind || caps->indn) && (caps->ri || caps->rin))
-    tty->ocaps |= TTCF_SCROLL;
-
   /* Erasure. */
-  if (caps->ech) tty->ocaps |= TTCF_ERCH;
-  if (caps->el1) tty->ocaps |= TTCF_ERBOL;
-  if (caps->el) tty->ocaps |= TTCF_EREOL;
-  if (caps->ed) tty->ocaps |= TTCF_EREOD;
+  if (t->cap.ech) t->tty.ocaps |= TTCF_ERCH;
+  if (t->cap.el1) t->tty.ocaps |= TTCF_ERBOL;
+  if (t->cap.el) t->tty.ocaps |= TTCF_EREOL;
+  if (t->cap.ed) t->tty.ocaps |= TTCF_EREOD;
+  if (t->cap.clear || (t->cap.ed && t->cap.home)) t->tty.ocaps |= TTCF_ERDSP;
 
   /* Insertion and deletion. */
-  if (caps->ich || caps->ich1) tty->ocaps |= TTCF_INSCH;
-  if (caps->il || caps->il1) tty->ocaps |= TTCF_INSLN;
-  if (caps->dch || caps->dch1) tty->ocaps |= TTCF_DELCH;
-  if (caps->dl || caps->dl1) tty->ocaps |= TTCF_DELLN;
+  if (t->cap.ich || t->cap.ich1) t->tty.ocaps |= TTCF_INSCH;
+  if (t->cap.il || t->cap.il1) t->tty.ocaps |= TTCF_INSLN;
+  if (t->cap.dch || t->cap.dch1) t->tty.ocaps |= TTCF_DELCH;
+  if (t->cap.dl || t->cap.dl1) t->tty.ocaps |= TTCF_DELLN;
+
+  /* Geometry. */
+  if (!t->tty.wd)
+    { wd = ops->cap.intcap(&t->tty, CAP_WD); if (wd > 0) t->tty.wd = wd; }
+  if (!t->tty.ht)
+    { ht = ops->cap.intcap(&t->tty, CAP_HT); if (ht > 0) t->tty.ht = ht; }
+}
+
+#define CHECK(expr) do { if ((expr) < 0) { rc = -1; goto end; } } while (0)
+
+#define PUT0V(npad, cap_)                                              \
+  CHECK(ops->cap.put0(&t->tty, gops, go, (npad), (cap_)))
+#define PUT1IV(npad, cap_, i0)                                         \
+  CHECK(ops->cap.put1i(&t->tty, gops, go, (npad), (cap_), (i0)))
+#define PUT2IV(npad, cap_, i0, i1)                                     \
+  CHECK(ops->cap.put2i(&t->tty, gops, go, (npad), (cap_), (i0), (i1)))
+
+#define PUT0(npad, name) PUT0V(npad, t->cap.name)
+#define PUT1I(npad, name, i0) PUT1IV(npad, t->cap.name, i0)
+#define PUT2I(npad, name, i0, i1) PUT2IV(npad, t->cap.name, i0, i1)
+
+static int caps_setcolour(struct tty_caps *t,
+                         const struct gprintf_ops *gops, void *go,
+                         const char *cap, uint32 spc, uint32 clr)
+{
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int rc;
+
+  switch (spc) {
+    case TTCSPC_1BPC: case TTCSPC_1BPCBR: PUT1IV(0, cap, clr);       break;
+    case TTCSPC_4LPC: case TTCSPC_6LPC:          PUT1IV(0, cap, clr +  16); break;
+    case TTCSPC_8LGS:                    PUT1IV(0, cap, clr +  80); break;
+    case TTCSPC_24LGS:                   PUT1IV(0, cap, clr + 232); break;
+
+    case TTCSPC_8BPC:
+      /* There's an unfortunate ambiguity in the `setaf' conventions.  The
+       * first eight colours should be dark shades of blue, but in fact
+       * they're interpreted as the one-bit-per-channel basic colours by
+       * common `terminfo' settings.  Notice and kludge by adding a little
+       * red.  This will tinge the colour magenta, but all such colours are
+       * so dark as to be effectively black anyway, so I doubt that this will
+       * be noticeable.
+       */
+      if (spc == TTCSPC_8BPC && clr < 8) clr += 65536;
+      PUT1IV(0, cap, clr); break;
+
+    /* case TTCSPC_NONE: */
+    default: rc = -1; goto end;
+  }
+  rc = 0;
+end:
+  return (rc);
 }
 
-static int caps_setattr(struct tty *tty, const struct tty_caps *caps,
+static int caps_setattr(struct tty *tty,
                        const struct gprintf_ops *gops, void *go,
                        const struct tty_attr *a)
 {
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
   struct tty_attr aa;
+  uint32 diff;
+  int rc;
+
+  /* Work out what needs doing. */
+  tty_clampattr(&aa, a, t->tty.acaps);
+  diff = aa.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.
+   */
+  if (((diff&TTAF_LNMASK) && !(aa.f&TTAF_LNMASK) && !t->cap.rmul) ||
+      ((diff&TTAF_WTMASK) && !(aa.f&TTAF_WTMASK)) ||
+      ((diff&~aa.f&TTAF_ITAL) && !t->cap.ritm) ||
+      (diff&~aa.f&TTAF_INVV))
+    { PUT0(0, sgr0); diff = aa.f; }
+
+  /* Line style. */
+  if (diff&TTAF_LNMASK)
+    switch ((aa.f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+      case TTLN_NONE: PUT0(0, rmul); break;
+      case TTLN_ULINE: PUT0(0, smul); break;
+      /* case TTLN_UULINE: */
+      /* case TTLN_STRIKE: */
+      default: rc = -1; goto end;
+    }
+
+  /* Text weight. */
+  if (diff&TTAF_WTMASK)
+    switch ((aa.f&TTAF_WTMASK) >> TTAF_WTSHIFT) {
+      /* case TTWT_MED: */
+      case TTWT_BOLD: PUT0(0, bold); break;
+      case TTWT_DIM: PUT0(0, dim); break;
+      default: rc = -1; goto end;
+    }
 
-  tty_clampattr(&aa, a, tty->acaps);
-  
+  /* Other text effects. */
+  if (diff&TTAF_ITAL) {
+    if (aa.f&TTAF_ITAL) PUT0(0, sitm);
+    else PUT0(0, ritm);
+  }
+  if (diff&aa.f&TTAF_INVV) PUT0(0, rev);
+
+  /* Colours. */
+  if (((diff&TTAF_FGSPCMASK) && !(aa.f&TTAF_FGSPCMASK)) ||
+      ((diff&TTAF_BGSPCMASK) && !(aa.f&TTAF_BGSPCMASK))) {
+    /* There's no capability string for resetting just the foreground
+     * or background colours to the defaults, so deal with that here.
+     */
+
+    PUT0(0, op);
+    diff = (diff&~(TTAF_FGSPCMASK | TTAF_BGSPCMASK)) |
+          (aa.f&(TTAF_FGSPCMASK | TTAF_BGSPCMASK));
+  }
+  if ((diff&TTAF_FGSPCMASK) || aa.fg != t->tty.st.attr.fg)
+    CHECK(caps_setcolour(t, gops, go, t->cap.setaf,
+                        (aa.f&TTAF_FGSPCMASK) >> TTAF_FGSPCSHIFT, aa.fg));
+  if ((diff&TTAF_BGSPCMASK) || aa.bg != t->tty.st.attr.bg)
+    CHECK(caps_setcolour(t, gops, go, t->cap.setab,
+                        (aa.f&TTAF_BGSPCMASK) >> TTAF_BGSPCSHIFT, aa.bg));
+
+  /* All done. */
+  rc = 0;
+end:
+  t->tty.st.attr = aa; return (rc);
 }
 
 static int caps_setmodes(struct tty *tty,
                         const struct gprintf_ops *gops, void *go,
-                        uint32 modes_bic, uint32 modes_xor);
+                        uint32 modes_bic, uint32 modes_xor)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  uint32 modes, diff;
+  const char *cap;
+  int rc;
+
+  /* Figure out which modes to set. */
+  modes = (t->tty.st.modes&~modes_bic) ^ modes_xor;
+  diff = modes ^ t->tty.st.modes;
+
+  /* Automatic margins. */
+  if (diff&TTMF_AUTOM) {
+    if (modes&TTMF_AUTOM) PUT0(0, smam);
+    else PUT0(0, rmam);
+  }
+
+  /* Full-screen. */
+  if (diff&TTMF_FSCRN) {
+    if (modes&TTMF_FSCRN) PUT0(0, smcup);
+    else PUT0(0, rmcup);
+  }
+
+  /* Cursor visibility. */
+  if (diff&TTMF_CVIS) {
+    if (modes&TTMF_CVIS) PUT0(0, civis);
+    else PUT0(0, cnorm);
+  }
+
+  /* Auto-insert. */
+  if (diff&TTMF_INS) {
+    cap = modes&TTMF_INS ? t->cap.smir : t->cap.rmir;
+    if (cap) PUT0V(0, cap);
+    else if (!t->cap.ich) { rc = -1; goto end; }
+  }
+
+  /* Delete characters. */
+  if (diff&TTMF_DEL) {
+    cap = modes&TTMF_DEL ? t->cap.smdc : t->cap.rmdc;
+    if (cap) PUT0V(0, cap);
+    else if (!t->cap.dch && !t->cap.dch1) { rc = -1; goto end; }
+  }
+
+  /* Done. */
+  rc = 0;
+end:
+  t->tty.st.modes = modes; return (rc);
+}
+
+#define CIF_PADMUL 1u
+
+static int caps_iterate(struct tty_caps *t,
+                       const struct gprintf_ops *gops, void *go,
+                       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);
+}
+
+static int caps_move_relative(struct tty_caps *t,
+                             const struct gprintf_ops *gops, void *go,
+                             int delta,
+                             const char *fw1, const char *fwn,
+                             const char *rv1, const char *rvn)
+{
+  const char *mv1, *mvn;
+
+  if (!delta) return (0);
+  else if (delta > 0) { mv1 = fw1; mvn = fwn; }
+  else { mv1 = rv1; mvn = rvn; delta = - delta; }
+  return (caps_iterate(t, gops, go, mv1, mvn, 0, 0, delta));
+}
+
 static int caps_move(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
-                    unsigned orig, int y, int x);
+                    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;
+  int rc;
+
+  if (!t->cap.mir && (t->tty.st.modes&TTMF_INS)) PUT0(0, rmir);
+
+  a = t->tty.st.attr;
+  if (!t->cap.msgr && a.f) { PUT0(0, sgr0); t->tty.st.attr.f = 0; }
+
+  switch (orig) {
+    case TTORG_HOME:
+      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, gops, go, x,
+                                  t->cap.cuf1, t->cap.cuf,
+                                  t->cap.cub1, t->cap.cub));
+       }
+      } else if (t->cap.home) {
+       PUT0(1, home);
+       CHECK(caps_iterate(t, gops, go, t->cap.cud1, t->cap.cud, 0, 1, y));
+       CHECK(caps_iterate(t, gops, go, t->cap.cuf1, t->cap.cuf, 0, 1, x));
+      } else
+       { rc = -1; goto end; }
+      break;
+
+    case TTORG_CUR:
+      CHECK(caps_move_relative(t, gops, go, y,
+                              t->cap.cud1, t->cap.cud,
+                              t->cap.cuu1, t->cap.cuu));
+      CHECK(caps_move_relative(t, gops, go, x,
+                              t->cap.cuf1, t->cap.cuf,
+                              t->cap.cub1, t->cap.cub));
+      break;
+
+    case TTOF_XHOME | TTOF_YCUR:
+      if (x == 0 && y == 1)
+       PUT0(1, nel);
+      else {
+       CHECK(caps_move_relative(t, gops, go, 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, gops, go, t->cap.cuf1, t->cap.cuf, 0, 1, x));
+       }
+      }
+      break;
+
+    case TTOF_XCUR | TTOF_YHOME:
+      PUT1I(1, vpa, y);
+      CHECK(caps_move_relative(t, gops, go, x,
+                              t->cap.cuf1, t->cap.cuf,
+                              t->cap.cub1, t->cap.cub));
+      break;
+
+    default:
+      rc = -1; goto end;
+      break;
+  }
+
+  if (!t->cap.mir && (t->tty.st.modes&TTMF_INS)) PUT0(0, smir);
+
+  if (!t->cap.msgr && t->tty.st.attr.f)
+    CHECK(caps_setattr(&t->tty, gops, go, &a));
+
+  rc = 0;
+end:
+  return (rc);
+}
+
 static int caps_repeat(struct tty *tty,
                       const struct gprintf_ops *gops, void *go,
-                      int ch, unsigned n);
+                      int ch, unsigned n)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  unsigned wd, nn;
+  int rc;
+
+  if (!t->cap.rep)
+    while (n--) CHECK(gops->putch(go, ch));
+  else {
+    wd = t->tty.wd;
+    while (n) {
+      nn = n; if (nn > INT_MAX) nn = INT_MAX;
+      PUT2I((nn + wd - 1)/wd, rep, ch, nn);
+      n -= nn;
+    }
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
 static int caps_erase(struct tty *tty,
                      const struct gprintf_ops *gops, void *go,
-                     unsigned f);
+                     unsigned f)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int rc;
+
+  if (f&TTEF_DSP)
+    switch (f&(TTEF_BEGIN | TTEF_END)) {
+      case 0:
+       break;
+      case TTEF_BEGIN | TTEF_END:
+       if (t->cap.clear) PUT0(t->tty.ht, clear);
+       else { PUT0(1, home); PUT0(t->tty.ht, ed); }
+       break;
+      case TTEF_END:
+       PUT0(t->tty.ht, ed);
+       break;
+      default:
+       rc = -1; goto end;
+       break;
+    }
+  else {
+    if (f&TTEF_BEGIN) PUT0(1, el1);
+    if (f&TTEF_END) PUT0(1, el);
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
 static int caps_erch(struct tty *tty,
                     const struct gprintf_ops *gops, void *go,
-                    unsigned n);
+                    unsigned n)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int rc;
+
+  if (n) PUT1I(1, ech, n);
+  rc = 0;
+end:
+  return (rc);
+}
+
 static int caps_ins(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
-                   unsigned f, unsigned n);
+                   unsigned f, unsigned n)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  int rc;
+
+  if (f&TTIDF_LN)
+    CHECK(caps_iterate(t, gops, go,
+                      t->cap.il1, t->cap.il, CIF_PADMUL, 1, n));
+  else
+    CHECK(caps_iterate(t, gops, go,
+                      t->cap.ich1, t->cap.ich, 0, 1, n));
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int caps_inch(struct tty *tty,
+                    const struct gprintf_ops *gops, void *go,
+                    int ch)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  const struct tty_capops *ops = (const struct tty_capops *)t->tty.ops;
+  int rc;
+
+  if (t->cap.smir ? !(t->tty.st.modes&TTMF_INS) : !t->cap.ich)
+    { rc = -1; goto end; }
+  if (t->cap.ich) PUT0(1, ich);
+  CHECK(gops->putch(go, ch));
+  if (t->cap.ip) PUT0(1, ip);
+  rc = 0;
+end:
+  return (rc);
+}
+
 static int caps_del(struct tty *tty,
                    const struct gprintf_ops *gops, void *go,
-                   unsigned f, unsigned n);
-static int caps_setscrgn(struct tty *tty,
-                        const struct gprintf_ops *gops, void *go,
-                        unsigned y0, unsigned y1);
-static int caps_scroll(struct tty *tty,
-                      const struct gprintf_ops *gops, void *go,
-                      int n);
+                   unsigned f, unsigned n)
+{
+  struct tty_caps *t = (struct tty_caps *)tty;
+  int rc;
+
+  if (n) {
+    if (f&TTIDF_LN)
+      CHECK(caps_iterate(t, gops, go,
+                        t->cap.dl1, t->cap.dl, CIF_PADMUL, 1, n));
+    else
+      CHECK(caps_iterate(t, gops, go,
+                        t->cap.dch1, t->cap.dch, 0, 1, n));
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
+#undef CHECK
+#undef PUT0V
+#undef PUT1IV
+#undef PUT2IV
+#undef PUT0
+#undef PUT1I
+#undef PUT2I
 
+#define TTY_CAPOPS                                                     \
+  caps_setattr, caps_setmodes,                                         \
+  caps_move, caps_repeat,                                              \
+  caps_erase, caps_erch, caps_ins, caps_inch, caps_del,                        \
+  0, 0, 0, 0
 
 #endif
 
+/*----- Termcap -----------------------------------------------------------*/
+
+#ifdef HAVE_TERMCAP
+
+struct tty_termcapslots {
+  char termbuf[4096], capbuf[4096], *capcur;
+};
+struct tty_termcap { TTY_CAPSPFX; struct tty_termcapslots tc; };
+union tty_termcapu { struct tty_termcap tc; TTY_CAPSUSFX; };
+
+static int termcap_boolcap(struct tty *tty,
+                          int uix, const char *info, const char *cap)
+{
+  int p;
+
+  p = tgetflag(cap); assert(p >= 0);
+  return (p);
+}
+
+static int termcap_intcap(struct tty *tty,
+                         int uix, const char *info, const char *cap)
+{
+  int n;
+
+  n = tgetnum(cap); assert(n >= -1);
+  return (n);
+}
+
+static const char *termcap_strcap(struct tty *tty,
+                                 int uix, const char *info,
+                                 const char *cap)
+{
+  struct tty_termcap *t = (struct tty_termcap *)tty;
+  const char *p;
+
+  p = tgetstr(cap, &t->tc.capcur); assert(p != (const char *)-1);
+  return (p);
+}
+
+static int termcap_put0(struct tty *tty,
+                       const struct gprintf_ops *gops, void *go,
+                       unsigned npad, const char *cap)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(cap, npad, caps_putch));
+}
+
+static int termcap_put1i(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go,
+                        unsigned npad, const char *cap, int i0)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(tgoto(cap, -1, i0), npad, caps_putch) == OK ? 0 : -1);
+}
+
+static int termcap_put2i(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go,
+                        unsigned npad,
+                        const char *cap, int i0, int i1)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(tgoto(cap, i1, i0), npad, caps_putch) == OK ? 0 : -1);
+}
+
+static const union tty_capopsu termcap_ops = { {
+  { caps_release, TTY_CAPOPS },
+  { termcap_boolcap, termcap_intcap, termcap_strcap,
+    termcap_put0, termcap_put1i, termcap_put2i }
+} };
+
+static struct tty *termcap_init(FILE *fp)
+{
+  union tty_termcapu *u = 0; struct tty *ret = 0;
+  const char *term;
+
+  if (global_lock)
+    { debug("termcap/terminfo terminal already open"); goto end; }
+  term = getenv("TERM"); if (!term) goto end;
+  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);
+  init_caps(&u->cap);
+  ret = &u->tty; u = 0;
+end:
+  xfree(u); global_lock = ret; return (ret);
+}
+
+#endif
+
+/*----- Terminfo ----------------------------------------------------------*/
+
+#ifdef HAVE_TERMINFO
+
+static int terminfo_boolcap(struct tty *tty,
+                           int uix, const char *info, const char *cap)
+{
+  int p;
+
+  p = tigetflag(info); assert(p >= 0);
+  return (p);
+}
+
+static int terminfo_intcap(struct tty *tty,
+                          int uix, const char *info, const char *cap)
+{
+  int n;
+
+  n = tigetnum(info); assert(n >= -1);
+  return (n);
+}
+
+static const char *terminfo_strcap(struct tty *tty,
+                                  int uix, const char *info,
+                                  const char *cap)
+{
+  const char *p;
+
+  p = tigetstr(info); assert(p != (const char *)-1);
+  return (p);
+}
+
+static int terminfo_put0(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go,
+                        unsigned npad, const char *cap)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(cap, npad, caps_putch));
+}
+
+static int terminfo_put1i(struct tty *tty,
+                         const struct gprintf_ops *gops, void *go,
+                         unsigned npad, const char *cap, int i0)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(tparm(cap, i0), npad, caps_putch) == OK ? 0 : -1);
+}
+
+static int terminfo_put2i(struct tty *tty,
+                         const struct gprintf_ops *gops, void *go,
+                         unsigned npad,
+                         const char *cap, int i0, int i1)
+{
+  if (!cap) return (-1);
+  global_gops = gops; global_gout = go;
+  return (tputs(tparm(cap, i0, i1), npad, caps_putch) == OK ? 0 : -1);
+}
+
+static const union tty_capopsu terminfo_ops = { {
+  { caps_release, TTY_CAPOPS },
+  { terminfo_boolcap, terminfo_intcap, terminfo_strcap,
+    terminfo_put0, terminfo_put1i, terminfo_put2i }
+} };
+
+static struct tty *terminfo_init(FILE *fp)
+{
+  union tty_capsu *u = 0; struct tty *ret = 0;
+  int err;
+
+  if (global_lock)
+    { debug("termcap/terminfo terminal already open"); goto end; }
+  if (setupterm(0, fp ? fileno(fp) : -1, &err) != OK || err < 1) goto end;
+  XNEW(u);
+  u->tty.ops = &terminfo_ops.tty;
+  common_init(&u->tty, fp);
+  init_caps(&u->cap);
+  ret = &u->tty; u = 0;
+end:
+  xfree(u); global_lock = ret; return (ret);
+}
+
+#endif
+
+/*----- Unibilium ---------------------------------------------------------*/
+
+#ifdef HAVE_UNIBILIUM
+
+struct tty_unibislots {
+  unibi_term *ut;
+  unibi_var_t dy[26], st[26];
+};
+struct tty_unibilium { TTY_CAPSPFX; struct tty_unibislots u; };
+union tty_unibiliumu { struct tty_unibilium u; TTY_CAPSUSFX; };
+
+static int termunibi_boolcap(struct tty *tty,
+                           int uix, const char *info, const char *cap)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  return (unibi_get_bool(t->u.ut, uix));
+}
+
+static int termunibi_intcap(struct tty *tty,
+                          int uix, const char *info, const char *cap)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  return (unibi_get_num(t->u.ut, uix));
+}
+
+static const char *termunibi_strcap(struct tty *tty,
+                                  int uix, const char *info,
+                                  const char *cap)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  return (unibi_get_str(t->u.ut, uix));
+}
+
+struct termunibi_outctx {
+  struct tty_unibilium *t;
+  const struct gprintf_ops *gops; void *go;
+  char pad[128];
+  int rc;
+};
+
+static void termunibi_putch(void *ctx, const char *p, size_t sz)
+{
+  struct termunibi_outctx *out = ctx;
+
+  if (out->gops->putm(out->go, p, sz)) out->rc = -1;
+}
+
+static void termunibi_pad(void *ctx, size_t ms, int mulp, int forcep)
+{
+  char pad[128];
+  struct termunibi_outctx *out = ctx;
+  struct tty_unibilium *t = out->t;
+  struct timeval tv;
+  size_t n, nn;
+
+  /* Based on 7 data bits, 1 stop bit, 1 parity bit. */
+#define BITS_PER_KB 9000
+
+  if (forcep || t->tty.baud >= t->cap.pb) {
+    if (t->cap.npc) {
+      tv.tv_sec = ms/1000; tv.tv_usec = 1000*(ms%1000);
+      if (t->tty.fpout) fflush(t->tty.fpout);
+      select(0, 0, 0, 0, &tv);
+    } else {
+      n = (ms*t->tty.baud + BITS_PER_KB - 1)/BITS_PER_KB;
+      while (n) {
+       if (n < sizeof(out->pad)) nn = n;
+       else nn = sizeof(out->pad);
+       if (out->gops->putm(out->go, pad, nn)) out->rc = -1;
+       n -= nn;
+      }
+    }
+  }
+
+#undef BITS_PER_KB
+}
+
+static void setup_termunibi_outctx(struct tty_unibilium *t,
+                                  struct termunibi_outctx *out,
+                                  const struct gprintf_ops *gops, void *go)
+{
+  out->t = t; out->rc = 0;
+  out->gops = gops; out->go = go;
+  if (!t->cap.npc)
+    memset(out->pad, t->cap.pad ? *t->cap.pad : 0, sizeof(out->pad));
+}
+
+static int termunibi_put0(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go,
+                        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);
+  setup_termunibi_outctx(t, &out, gops, go);
+  unibi_format(t->u.dy, t->u.st, cap, arg,
+              termunibi_putch, &out,
+              termunibi_pad, &out);
+  return (out.rc);
+}
+
+static int termunibi_put1i(struct tty *tty,
+                          const struct gprintf_ops *gops, void *go,
+                          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);
+  setup_termunibi_outctx(t, &out, gops, go);
+  arg[0] = unibi_var_from_num(i0);
+  unibi_format(t->u.dy, t->u.st, cap, arg,
+              termunibi_putch, &out,
+              termunibi_pad, &out);
+  return (out.rc);
+}
+
+static int termunibi_put2i(struct tty *tty,
+                          const struct gprintf_ops *gops, void *go,
+                          unsigned npad,
+                          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);
+  setup_termunibi_outctx(t, &out, gops, go);
+  arg[0] = unibi_var_from_num(i0);
+  arg[1] = unibi_var_from_num(i1);
+  unibi_format(t->u.dy, t->u.st, cap, arg,
+              termunibi_putch, &out,
+              termunibi_pad, &out);
+  return (out.rc);
+}
+
+static void termunibi_release(struct tty *tty)
+{
+  struct tty_unibilium *t = (struct tty_unibilium *)tty;
+
+  unibi_destroy(t->u.ut);
+}
+
+static const union tty_capopsu termunibi_ops = { {
+  { termunibi_release, TTY_CAPOPS },
+  { termunibi_boolcap, termunibi_intcap, termunibi_strcap,
+    termunibi_put0, termunibi_put1i, termunibi_put2i }
+} };
+
+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;
+  XNEW(u);
+  u->tty.ops = &termunibi_ops.tty;
+  u->u.u.ut = ut; ut = 0;
+  common_init(&u->tty, fp);
+  init_caps(&u->cap);
+  ret = &u->tty; u = 0;
+end:
+  xfree(u); if (ut) unibi_destroy(ut);
+  return (ret);
+}
+
+#endif
+
+/*----- 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.
+ *
+ *   * CUP: \33 [ Y ; X H              `cursor position' [vt100]
+ *
+ *   * CUU/CUD/CUR/CUL: \33 [ N A/B/C/D        `cursor up/down/right/left'
+ *
+ *   * DCH: \33 [ N P                  `delete character' [vt220]
+ *                                             (single char only in vt102?)
+ *
+ *   * DL: \33 [ N M                   `delete line' [vt220]
+ *                                             (single line only in vt102?)
+ *
+ *   * ECH: \33 [ N X                  `erase characters' [vt220]
+ *
+ *   * ED: \33 [ P J                   `erase in display'
+ *      P = 0                          erase to end-of-screen [vt100]
+ *      P = 1                          erase from start-of-screen [vt100]
+ *      P = 2                          erase entire screen [vt100]
+ *
+ *   * EL: \33 [ P K                   `erase in line'
+ *      P = 0                          erase to end-of-line [vt100]
+ *      P = 1                          erase from start-of-line [vt100]
+ *      P = 2                          erase entire line [vt100]
+ *
+ *   * HPA/VPA: \33 [ I G/d            `horizontal/vertical position
+ *                                             absolute' [ecma48-4]
+ *
+ *   * ICH: \33 [ N @                  `insert character' [vt220]
+ *                                             (single char only in vt102?)
+ *
+ *   * IL: \33 [ N L                   `insert line' [vt220]
+ *                                             (single line only in vt102?)
+ *
+ *   * SGR: \33 [ P ; ... m            `select graphics rendition'
+ *      P = 0                          cancel all attributes [vt100]
+ *       P = 1                         bold [vt100]
+ *      P = 2                          dim [ecma48-4]
+ *      P = 3                          italics [ecma48-4]
+ *      P = 4                          underline [vt100]
+ *      P = 7                          inverse video [vt100]
+ *      P = 9                          strikeout [ecma48-4]
+ *      P = 21                         double underline [ecma48-4]
+ *      P = 22                         cancal bold/dim [vt220]
+ *      P = 24                         cancel underline [vt220]
+ *      P = 27                         cancel inverse video [vt220]
+ *      P = 30 + 4 R + 2 G + B         set 1BPC foreground [ecma48-4]
+ *      P = 38 : 2 : ? : R : G : B     set foreground [iso8613-6]
+ *      P = 38 : 5 : N                 set foreground [iso8613-6, xterm]
+ *      P = 39                         cancel foreground [ecma48-4]
+ *      P = 40--49                     as above, for background
+ *      P = 90 + 4 R + 2 G + B         set bright 1BPC foreground [xterm]
+ *      P = 100 + 4 R + 2 G + B        set bright 1BPC background [xterm]
+ *
+ *   * SM/RM: \33 [ P ; ... h/l        `set/reset modes'
+ *      M = 4                          insert [vt220]
+ *
+ *   * SM, RM: \33 [ ? P ; ... h/l     `set/reset private modes'
+ *      M = 7                          auto right margin [vt100]
+ *      M = 25                         visible cursor [vt220]
+ *      M = 1049                       alternate screen [xterm]
+ *
+ *   * \33 [ P ; X ; Y t               `window manipulation'
+ *      P = 22, X = 0                  save title and icon [xterm]
+ *      P = 23, X = 0                  restore title and icon [xterm]
+ */
+
+static void ansi_release(struct tty *tty) { ; }
+
+#define CHECK(expr) do { if ((expr) < 0) { rc = -1; goto end; } } while (0)
+
+#define PUTCH(ch) CHECK(gops->putch(go, (ch)))
+#define PUTLIT(lit) CHECK(gops->putm(go, (lit), sizeof(lit) - 1))
+#define SEMI do {                                                      \
+  if (!(f&TAF_SEMI)) f |= TAF_SEMI;                                    \
+  else PUTCH(';');                                                     \
+} while (0)
+
+static int ansi_setcolour(struct tty_ansi *t, unsigned *f_inout,
+                         const struct gprintf_ops *gops, void *go,
+                         int norm, int br,
+                         uint32 spc, uint32 clr)
+{
+  unsigned f = *f_inout;
+  int rc;
+
+  switch (spc) {
+    case TTCSPC_NONE:
+      SEMI; CHECK(gprintf(gops, go, "%d", norm + 9));
+      break;
+    case TTCSPC_1BPC:
+      SEMI; CHECK(gprintf(gops, go, "%d", norm + clr));
+      break;
+    case TTCSPC_1BPCBR:
+      SEMI; CHECK(gprintf(gops, go, "%d", br + (clr&~TT1BPC_BRI)));
+      break;
+    case TTCSPC_4LPC: case TTCSPC_6LPC:
+      SEMI;
+      if (t->ansi.f&TAF_SEMI)
+       CHECK(gprintf(gops, go, "%d;5;%d", norm + 8, clr + 16));
+      else
+       CHECK(gprintf(gops, go, "%d:5:%d", norm + 8, clr + 16));
+      break;
+    case TTCSPC_8LGS:
+      SEMI;
+      if (t->ansi.f&TAF_SEMI)
+       CHECK(gprintf(gops, go, "%d;5;%d", norm + 8, clr + 80));
+      else
+       CHECK(gprintf(gops, go, "%d:5:%d", norm + 8, clr + 80));
+      break;
+    case TTCSPC_24LGS:
+      SEMI;
+      if (t->ansi.f&TAF_SEMI)
+       CHECK(gprintf(gops, go, "%d;5;%d", norm + 8, clr + 232));
+      else
+       CHECK(gprintf(gops, go, "%d:5:%d", norm + 8, clr + 232));
+      break;
+    case TTCSPC_8BPC:
+      SEMI;
+      if (t->ansi.f&TAF_SEMI)
+       CHECK(gprintf(gops, go, "%d;2;%d;%d;%d", norm + 8,
+                     TTCOL_8BR(clr), TTCOL_8BG(clr), TTCOL_8BB(clr)));
+      else
+       CHECK(gprintf(gops, go, "%d:2::%d:%d:%d", norm + 8,
+                     TTCOL_8BR(clr), TTCOL_8BG(clr), TTCOL_8BB(clr)));
+      break;
+    default:
+      rc = -1; goto end;
+  }
+
+  rc = 0;
+end:
+  *f_inout = f; return (rc);
+}
+
+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;
+  struct tty_attr aa;
+  uint32 diff;
+  int rc = 0;
+  unsigned z, c, f = 0;
+
+  tty_clampattr(&aa, a, t->tty.acaps);
+  diff = aa.f ^ t->tty.st.attr.f;
+  if (!diff && aa.fg == t->tty.st.attr.fg && aa.bg == t->tty.st.attr.bg)
+    return (0);
+
+  c = 0;
+#define CLEARP(mask) ((diff&(mask)) && !(aa.f&(mask)))
+  if (CLEARP(TTAF_LNMASK)) c += 3;
+  if (CLEARP(TTAF_WTMASK)) c += 3;
+  if (diff&~aa.f&TTAF_INVV) c += 3;
+  if (diff&~aa.f&TTAF_STRIKE) c += 3;
+  if (diff&~aa.f&TTAF_ITAL) c += 3;
+  if (CLEARP(TTAF_FGSPCMASK)) c += 3;
+  if (CLEARP(TTAF_BGSPCMASK)) c += 3;
+#undef CLEARP
+
+  z = 0;
+  switch ((aa.f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+    case TTLN_ULINE: z += 2; break;
+    case TTLN_UULINE: z += 3; break;
+  }
+  if (aa.f&TTAF_WTMASK) z += 2;
+  if (aa.f&TTAF_INVV) z += 2;
+  if (aa.f&TTAF_STRIKE) z += 2;
+  if (aa.f&TTAF_ITAL) z += 2;
+#define COLOURCOST(col) do {                                           \
+  switch ((aa.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;                                  \
+  }                                                                    \
+} while (0)
+  COLOURCOST(FG); COLOURCOST(BG);
+#undef COLOURCOST
+
+  PUTLIT("\33[");
+
+  if (z <= c) { SEMI; diff = aa.f; }
+
+  if (diff&TTAF_LNMASK)
+    switch ((aa.f&TTAF_LNMASK) >> TTAF_LNSHIFT) {
+      case TTLN_NONE: SEMI; PUTLIT("24"); break;
+      case TTLN_ULINE: SEMI; PUTCH('4'); break;
+      case TTLN_UULINE: SEMI; PUTLIT("21"); break;
+      default: rc = -1; goto end;
+    }
+
+  if (diff&TTAF_WTMASK)
+    switch ((aa.f&TTAF_WTMASK) >> TTAF_WTSHIFT) {
+      case TTWT_MED: SEMI; PUTLIT("22"); break;
+      case TTWT_BOLD: SEMI; PUTCH('1'); break;
+      case TTWT_DIM: SEMI; PUTCH('2'); break;
+      default: rc = -1; goto end;
+    }
+
+  if (diff&TTAF_INVV)
+    { SEMI; if (aa.f&TTAF_INVV) PUTCH('7'); else PUTLIT("27"); }
+  if (diff&TTAF_STRIKE)
+    { SEMI; if (aa.f&TTAF_STRIKE) PUTCH('9'); else PUTLIT("29"); }
+  if (diff&TTAF_ITAL)
+    { SEMI; if (aa.f&TTAF_ITAL) PUTCH('3'); else PUTLIT("23"); }
+
+  if (diff&TTAF_FGSPCMASK || aa.fg != tty->st.attr.fg)
+    CHECK(ansi_setcolour(t, &f, gops, go, 30, 90,
+                        (aa.f&TTAF_FGSPCMASK) >> TTAF_FGSPCSHIFT, aa.fg));
+  if (diff&TTAF_BGSPCMASK || aa.bg != tty->st.attr.bg)
+    CHECK(ansi_setcolour(t, &f, gops, go, 40, 100,
+                        (aa.f&TTAF_BGSPCMASK) >> TTAF_BGSPCSHIFT, aa.bg));
+
+  PUTCH('m'); rc = 0;
+end:
+  t->tty.st.attr = aa; return (rc);
+
+}
+
+static int ansi_setmodes(struct tty *tty,
+                        const struct gprintf_ops *gops, void *go,
+                        uint32 modes_bic, uint32 modes_xor)
+{
+  uint32 modes, diff;
+  int rc;
+
+  /* Figure out which modes to set. */
+  modes = (tty->st.modes&~modes_bic) ^ modes_xor;
+  diff = modes ^ tty->st.modes;
+
+  if (diff&TTMF_AUTOM) {
+    if (modes&TTMF_AUTOM) PUTLIT("\33[?7h");
+    else PUTLIT("\33[?7l");
+  }
+
+  if (diff&TTMF_FSCRN) {
+    if (modes&TTMF_FSCRN) PUTLIT("\33[?1049h\33[22;0;0t");
+    else PUTLIT("\33[?1049l\33[23;0;0t");
+  }
+
+  if (diff&TTMF_CVIS) {
+    if (modes&TTMF_CVIS) PUTLIT("\33[?25h");
+    else PUTLIT("\33[?25l");
+  }
+
+  if (diff&TTMF_INS) {
+    if (modes&TTMF_INS) PUTLIT("\33[4h");
+    else PUTLIT("\33[4l");
+  }
+
+  rc = 0;
+end:
+  tty->st.modes = modes;
+  return (rc);
+}
+
+static int ansi_move(struct tty *tty,
+                    const struct gprintf_ops *gops, void *go,
+                    unsigned orig, int y, int x)
+{
+  int rc;
+
+  if (orig == TTORG_HOME) {
+    if (!x) {
+      if (!y) PUTLIT("\33[H");
+      else CHECK(gprintf(gops, go, "\33[%dH", y + 1));
+    } else {
+      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)
+    PUTLIT("\r\n");
+  else {
+    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));
+    if (!(orig&TTOF_XCUR)) {
+      if (!x)
+       PUTCH('\r');
+      else if (tty->ocaps&TTCF_MIXMV)
+       CHECK(gprintf(gops, go, "\33[%dG", x + 1));
+      else
+       CHECK(gprintf(gops, go, "\r\33[%dC", x));
+    } else {
+      if (x == -1) PUTCH('\b');
+      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));
+    }
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int ansi_repeat(struct tty *tty,
+                      const struct gprintf_ops *gops, void *go,
+                      int ch, unsigned n)
+{
+  int rc;
+
+  while (n--) PUTCH(ch);
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int ansi_erase(struct tty *tty,
+                     const struct gprintf_ops *gops, void *go,
+                     unsigned f)
+{
+  int rc;
+
+  if (f&TTEF_DSP)
+    switch (f&(TTEF_BEGIN | TTEF_END)) {
+      case 0: break;
+      case TTEF_BEGIN: PUTLIT("\33[1J"); break;
+      case TTEF_END: PUTLIT("\33[J"); break;
+      case TTEF_BEGIN | TTEF_END: PUTLIT("\33[2J"); break;
+    }
+  else
+    switch (f&(TTEF_BEGIN | TTEF_END)) {
+      case 0: break;
+      case TTEF_BEGIN: PUTLIT("\33[1K"); break;
+      case TTEF_END: PUTLIT("\33[K"); break;
+      case TTEF_BEGIN | TTEF_END: PUTLIT("\33[2K"); break;
+    }
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int ansi_erch(struct tty *tty,
+                    const struct gprintf_ops *gops, void *go,
+                    unsigned n)
+{
+  int rc;
+
+  if (n == 1) PUTLIT("\33[X");
+  else if (n) CHECK(gprintf(gops, go, "\33[%uX", n));
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int ansi_ins(struct tty *tty,
+                   const struct gprintf_ops *gops, void *go,
+                   unsigned f, unsigned n)
+{
+  int rc;
+
+  if (f&TTIDF_LN) {
+    if (n == 1) PUTLIT("\33[L");
+    else if (n) CHECK(gprintf(gops, go, "\33[%uL", n));
+  } else {
+    if (n == 1) PUTLIT("\33[@");
+    else if (n) CHECK(gprintf(gops, go, "\33[%u@", n));
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
+static int ansi_inch(struct tty *tty,
+                    const struct gprintf_ops *gops, void *go,
+                    int ch)
+{
+  if (!(tty->st.modes&TTMF_INS)) return (-1);
+  else return (gops->putch(go, ch));
+}
+
+static int ansi_del(struct tty *tty,
+                   const struct gprintf_ops *gops, void *go,
+                   unsigned f, unsigned n)
+{
+  int rc;
+
+  if (f&TTIDF_LN) {
+    if (n == 1) PUTLIT("\33[M");
+    else if (n) CHECK(gprintf(gops, go, "\33[%uM", n));
+  } else {
+    if (n == 1) PUTLIT("\33[P");
+    else if (n) CHECK(gprintf(gops, go, "\33[%uP", n));
+  }
+  rc = 0;
+end:
+  return (rc);
+}
+
+#undef PUTCH
+#undef PUTLIT
+#undef SEMI
+
+#undef CHECK
+
+static const struct tty_ops ansi_ops = {
+  ansi_release,
+  ansi_setattr, ansi_setmodes,
+  ansi_move, ansi_repeat,
+  ansi_erase, ansi_erch, ansi_ins, ansi_inch, ansi_del,
+  0, 0, 0, 0
+};
+
+static struct tty *ansi_init(FILE *fp)
+{
+#define COLS_NO 0
+#define COLS_8 (TTACF_FG | TTACF_BG | TTACF_1BPC)
+#define COLS_16 (COLS_8 | TTACF_1BPCBR)
+#define COLS_88 (COLS_16 | TTACF_4LPC | TTACF_8LGS)
+#define COLS_256 (COLS_16 | TTACF_6LPC | TTACF_24LGS)
+#define COLS_16M (COLS_256 | TTACF_8BPC)
+
+#define EDIT_OPS (TTCF_ERCH |                                          \
+                 TTCF_DELCH | TTCF_DELLN |                             \
+                 TTCF_INSCH | TTCF_INSLN)
+
+  static const struct flagmap {
+    const char *name;
+    uint32 acaps, ocaps;
+    unsigned tf;
+  } flagmap[] = {
+    { "dim",           TTACF_DIM,      0,              0 },
+    { "uuline",                TTACF_UULINE,   0,              0 },
+    { "strike",                TTACF_STRIKE,   0,              0 },
+    { "ital",          TTACF_ITAL,     0,              0 },
+    { "cvis",          0,              TTMF_CVIS,      0 },
+    { "fscrn",         0,              TTMF_FSCRN,     0 },
+    { "insmode",       0,              TTMF_INS,       0 },
+    { "hvpa"   ,       0,              TTCF_MIXMV,     0 },
+    { "edit",          0,              EDIT_OPS,       0 },
+    { "cncattr",       0,              0,              TAF_CNCATTR },
+    { "editn",         0,              0,              TAF_EDITN },
+    { "semi",          0,              0,              TAF_SEMI },
+    { 0,               0,              0,              0 }
+  };
+
+#undef EDIT_OPS
+
+  static const struct kw { const char *name; uint32 val; }
+    kw_colours[] = {
+      { "no",                  COLS_NO },
+      { "8",                   COLS_8 },
+      { "16",                  COLS_16 },
+      { "88",                  COLS_88 },
+      { "256",                 COLS_256 },
+      { "16m",                 COLS_16M },
+      { 0,                     0 }
+    };
+
+  static const struct enummap {
+    const char *name;
+    uint32 mask;
+    const struct kw *kw;
+  } enummap[] = {
+    { "colours",               TTACF_CSPCMASK,         kw_colours },
+    { 0,                       0,                      0 }
+  };
+
+
+  static const struct termmap {
+    const char *pat;
+    unsigned acaps, ocaps, tf;
+  } termmap[] = {
+
+#define VT100_ACAPS (TTACF_ULINE | TTACF_BOLD | TTACF_INVV)
+#define VT100_OCAPS (TTMF_AUTOM |                                      \
+                    TTCF_RELMV | TTCF_ABSMV |                          \
+                    TTCF_MMARG |                                       \
+                    TTCF_ERBOL | TTCF_EREOL |                          \
+                    TTCF_ERBOD | TTCF_EREOD | TTCF_ERDSP)
+#define VT100_TF (0)
+
+#define VT102_ACAPS (VT100_ACAPS)
+#define VT102_OCAPS (VT100_OCAPS |                                     \
+                    TTMF_INS |                                         \
+                    TTCF_INSCH | TTCF_INSLN | TTCF_DELCH | TTCF_DELLN)
+#define VT102_TF (VT100_TF)
+
+#define VT220_ACAPS (VT102_ACAPS)
+#define VT220_OCAPS (VT102_OCAPS | TTMF_CVIS | TTCF_ERCH)
+#define VT220_TF (VT102_TF | TAF_CNCATTR | TAF_EDITN)
+
+#define ECMA48_ACAPS (VT220_ACAPS | TTACF_DIM)
+#define ECMA48_OCAPS (VT220_OCAPS | TTCF_MIXMV)
+#define ECMA48_TF (VT220_TF)
+
+#define XTERM_ACAPS (ECMA48_ACAPS)
+#define XTERM_OCAPS (ECMA48_OCAPS | TTMF_FSCRN)
+#define XTERM_TF (ECMA48_TF)
+
+#define STRIKE TTACF_STRIKE
+#define ITAL TTACF_ITAL
+#define SEMI TAF_SEMI
+
+#define T(pat, base, cols, acaps, ocaps, tf)                           \
+  { pat,                                                               \
+    base##_ACAPS | COLS_##cols | (acaps),                              \
+    base##_OCAPS | (ocaps), base##_TF | (tf) }
+
+    T("color_xterm",   XTERM,  8,      STRIKE | ITAL,  0,      0),
+
+    T("gnome",         XTERM,  16M,    STRIKE | ITAL,  0,      SEMI),
+  /*T("gonme-*"                XTERM,  16M,    STRIKE | ITAL,  0,      SEMI),*/
+
+    T("linux",         XTERM,  16,     0,              0,      0),
+
+    T("putty",         XTERM,  16M,    0,              0,      SEMI),
+
+    T("vt100*",                VT100,  NO,     0,              0,      0),
+    T("vt102*",                VT102,  NO,     0,              0,      0),
+    T("vt[2-5][0-9][0-9]*", VT220, NO, 0,              0,      0),
+
+    T("vte",           XTERM,  16M,    STRIKE | ITAL,  0,      SEMI),
+  /*T("vte-*"          XTERM,  16M,    STRIKE | ITAL,  0,      SEMI),*/
+
+    T("win",           XTERM,  16M,    0,              0,      SEMI),
+
+    T("xterm",         XTERM,  16M,    STRIKE | ITAL,  0,      0),
+    T("xterm-color",   XTERM,  8,      STRIKE | ITAL,  0,      0),
+    T("xterm-16color", XTERM,  16,     STRIKE | ITAL,  0,      0),
+    T("xterm-88color", XTERM,  88,     STRIKE | ITAL,  0,      SEMI),
+    T("xterm-256color",        XTERM,  256,    STRIKE | ITAL,  0,      SEMI),
+    T("xterm-direct",  XTERM,  16M,    STRIKE | ITAL,  0,      0),
+    T("xterm-*",       XTERM,  16M,    STRIKE | ITAL,  0,      0),
+
+  /*T("*-color",       XTERM,  16,     0,              0,      0),*/
+  /*T("*-16color",     XTERM,  16,     0,              0,      0),*/
+    T("*-88color",     XTERM,  88,     0,              0,      SEMI),
+    T("*-256color",    XTERM,  256,    0,              0,      SEMI),
+    T("*-direct",      XTERM,  16M,    0,              0,      SEMI),
+
+    T("*",             XTERM,  16,     0,              0,      0),
+    { 0,                               0,              0,      0 }
+
+#undef VT100_ACAPS
+#undef VT100_OCAPS
+#undef VT100_TF
+
+#undef VT102_ACAPS
+#undef VT102_OCAPS
+#undef VT102_TF
+
+#undef VT220_ACAPS
+#undef VT220_OCAPS
+#undef VT220_TF
+
+#undef ECMA48_ACAPS
+#undef ECMA48_OCAPS
+#undef ECMA48_TF
+
+#undef XTERM_ACAPS
+#undef XTERM_OCAPS
+#undef XTERM_TF
+
+#undef STRIKE
+#undef ITAL
+#undef SEMI
+  };
+
+#undef COLS_NO
+#undef COLS_8
+#undef COLS_16
+#undef COLS_88
+#undef COLS_256
+#undef COLS_16M
+
+  union tty_ansiu *u = 0; struct tty *ret = 0;
+  const char *term, *config, *p, *l;
+  const struct kw *kw;
+  const struct enummap *em;
+  const struct flagmap *fm;
+  const struct termmap *tm;
+  size_t n, nn;
+  unsigned
+    acaps = 0, ocaps = 0, tf = 0,
+    acapset = 0, ocapset = 0, tfset = 0,
+    f = 0;
+#define f_sense 1u
+
+  config = getenv("MLIB_TTY_ANSICONFIG");
+  term = getenv("TERM");
+
+  if (term && STRCMP(term, ==, "dumb")) goto end;
+
+  if (config) {
+    l = config + strlen(config);
+    for (;;) {
+
+      for (;;)
+       if (config >= l) goto done_config;
+       else if (!ISSPACE(*config)) break;
+       else config++;
+
+      for (p = config + 1; p < l && !ISSPACE(*p); p++);
+      if (*config == '+' || *config == '-') {
+       if (*config == '+') f |= f_sense;
+       else f &= ~f_sense;
+       config++; n = p - config;
+
+       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:
+       if ((acapset&fm->acaps) || (ocapset&fm->ocaps) || (tfset&fm->tf)) {
+         debug("duplicate setting for `%s'", fm->name);
+         goto next_config;
+       }
+       if (f&f_sense)
+         { acaps |= fm->acaps; ocaps |= fm->ocaps; tf |= fm->tf; }
+       acapset |= fm->acaps; ocapset |= fm->ocaps; tfset |= fm->tf;
+      } else {
+       n = p - config;
+       p = memchr(config, '=', n);
+       if (!p) {
+         debug("missing `=' in setting `%.*s'", (int)n, config);
+         goto next_config;
+       }
+       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:
+       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;
+       }
+       acaps |= kw->val; acapset |= em->mask;
+      }
+
+    next_config:
+      config += n;
+    }
+  done_config:;
+  }
+
+  if (term) {
+    for (tm = termmap; tm->pat; tm++)
+      if (str_match(tm->pat, term))
+       goto found_term;
+    assert(0);
+  found_term:
+    acaps |= tm->acaps&~acapset;
+    ocaps |= tm->ocaps&~ocapset;
+    tf |= tm->tf&~tfset;
+  }
+
+  env_colour_caps(&acaps);
+  if (acaps&TTACF_CSPCMASK) ocaps |= TTCF_BGER;
+
+  XNEW(u);
+  u->tty.ops = &ansi_ops;
+  u->tty.acaps = acaps;
+  u->tty.ocaps = ocaps;
+  u->ansi.ansi.f = tf;
+  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);
+  ret = &u->tty; u = 0;
+end:
+  xfree(u); return (ret);
+
+#undef f_sense
+}
+
+/*----- Backend selection -------------------------------------------------*/
+
+struct tty *tty_open(FILE *fp, unsigned f, const unsigned *backends)
+{
+  static const struct betab {
+    const char *name; unsigned code;
+    struct tty *(*init)(FILE */*fp*/);
+  } betab[] = {
+    { "unibilium",     TTBK_UNIBI,     termunibi_init },
+    { "terminfo",      TTBK_TERMINFO,  terminfo_init },
+    { "termcap",       TTBK_TERMCAP,   termcap_init },
+    { "ansi",          TTBK_ANSI,      ansi_init },
+    { 0,               0,              0 }
+  };
+
+  const struct betab *bt;
+  const char *config, *p, *l;
+  struct tty *tty = 0;
+  FILE *fpin = 0;
+  size_t n;
+
+  if (fp || !(f&TTF_OPEN))
+    fpin = fp != stdout ? fp : isatty(STDIN_FILENO) ? stdin : 0;
+  else {
+    if (isatty(STDIN_FILENO)) fpin = stdin;
+    else fpin = 0;
+    if (isatty(STDOUT_FILENO)) { fp = stdout; f |= TTF_BORROW; }
+    else if (isatty(STDERR_FILENO)) { fp = stderr; f |= TTF_BORROW; }
+    else {
+      fp = fopen("/dev/tty", "r+"); if (!fp) goto end;
+      f &= ~TTF_BORROW;
+    }
+  }
+
+  config = getenv("MLIB_TTY_BACKENDS");
+  if (config) {
+    l = config + strlen(config);
+    for (;;) {
+      for (;;)
+       if (config >= l) goto done_config;
+       else if (!ISSPACE(*config)) break;
+       else config++;
+
+      for (p = config + 1; p < l && !ISSPACE(*p); p++);
+      n = p - config;
+
+      for (bt = betab; bt->name; bt++)
+       if (STRNCMP(config, ==, bt->name, n) && !bt->name[n])
+         goto found_byname;
+      debug("unknown backend `%.*s'", (int)n, config); goto next_config;
+    found_byname:
+      tty = bt->init(fp); if (tty) goto found;
+      debug("failed to initialize `%s'", bt->name);
+    next_config:
+      config += n;
+    }
+  done_config:;
+  } else if (backends)
+    while (*backends) {
+      for (bt = betab; bt->name; bt++)
+       if (*backends == bt->code) goto found_bycode;
+      debug("unknown backend code %u", *backends); goto next_code;
+    found_bycode:
+      tty = bt->init(fp); if (tty) goto found;
+      debug("failed to initialize `%s'", bt->name);
+    next_code:
+      backends++;
+    }
+  else
+    for (bt = betab; bt->name; bt++) {
+      tty = bt->init(fp); if (tty) goto found;
+      debug("failed to initialize `%s'", bt->name);
+    }
+
+  debug("all backends failed"); goto end;
+found:
+  debug("selected backend `%s'", bt->name);
+  tty->fpin = fpin; tty->f = f; fp = 0;
+end:
+  if (fp && !(f&TTF_BORROW)) fclose(fp);
+  return (tty);
+}
+
+void tty_close(struct tty *tty)
+{
+  if (tty) {
+    if (tty->fpout && !(tty->f&TTF_BORROW)) fclose(tty->fpout);
+    tty->ops->release(tty); xfree(tty);
+  }
+}
+
+/*----- Terminal operations -----------------------------------------------*/
+
+int tty_setattrg(struct tty *tty,
+                const struct gprintf_ops *gops, void *go,
+                const struct tty_attr *a)
+  { return (tty->ops->setattr(tty, gops, go, a)); }
+
+int tty_setattr(struct tty *tty, const struct tty_attr *a)
+  { return (tty->ops->setattr(tty, &file_printops, tty->fpout, a)); }
+
+int tty_setattrlistg(struct tty *tty,
+                    const struct gprintf_ops *gops, void *go,
+                    const struct tty_attrlist *aa)
+{
+  for (;; aa++)
+    if ((tty->acaps&aa->cap_mask) == aa->cap_eq)
+      return (tty->ops->setattr(tty, gops, go, &aa->attr));
+    else if (!aa->cap_mask)
+      return (0);
+}
+
+int tty_setattrlist(struct tty *tty, const struct tty_attrlist *aa)
+  { return (tty_setattrlistg(tty, &file_printops, tty->fpout, aa)); }
+
+int tty_setmodesg(struct tty *tty,
+                 const struct gprintf_ops *gops, void *go,
+                 uint32 modes_bic, uint32 modes_xor)
+  { return (tty->ops->setmodes(tty, gops, go, modes_bic, modes_xor)); }
+
+int tty_setmodes(struct tty *tty, uint32 modes_bic, uint32 modes_xor)
+{
+  return (tty->ops->setmodes(tty, &file_printops, tty->fpout,
+                            modes_bic, modes_xor));
+}
+
+int tty_moveg(struct tty *tty,
+             const struct gprintf_ops *gops, void *go,
+             unsigned orig, int y, int x)
+  { return (tty->ops->move(tty, gops, go, orig, y, x)); }
+
+int tty_move(struct tty *tty, unsigned orig, int y, int x)
+  { return (tty->ops->move(tty, &file_printops, tty->fpout, orig, y, x)); }
+
+int tty_repeatg(struct tty *tty,
+               const struct gprintf_ops *gops, void *go,
+               int ch, unsigned n)
+  { return (tty->ops->repeat(tty, gops, go, ch, n)); }
+
+int tty_repeat(struct tty *tty, int ch, unsigned n)
+  { return (tty->ops->repeat(tty, &file_printops, tty->fpout, ch, n)); }
+
+int tty_eraseg(struct tty *tty,
+              const struct gprintf_ops *gops, void *go,
+              unsigned f)
+  { return (tty->ops->erase(tty, gops, go, f)); }
+
+int tty_erase(struct tty *tty, unsigned f)
+  { return (tty->ops->erase(tty, &file_printops, tty->fpout, f)); }
+
+int tty_erchg(struct tty *tty,
+             const struct gprintf_ops *gops, void *go,
+             unsigned n)
+  { return (tty->ops->erch(tty, gops, go, n)); }
+
+int tty_erch(struct tty *tty, unsigned n)
+  { return (tty->ops->erch(tty, &file_printops, tty->fpout, n)); }
+
+int tty_insg(struct tty *tty,
+            const struct gprintf_ops *gops, void *go,
+            unsigned f, unsigned n)
+  { return (tty->ops->ins(tty, gops, go, f, n)); }
+
+int tty_ins(struct tty *tty, unsigned f, unsigned n)
+  { return (tty->ops->ins(tty, &file_printops, tty->fpout, f, n)); }
+
+int tty_inchg(struct tty *tty,
+             const struct gprintf_ops *gops, void *go,
+             int ch)
+  { return (tty->ops->inch(tty, gops, go, ch)); }
+
+int tty_inch(struct tty *tty, int ch)
+  { return (tty->ops->inch(tty, &file_printops, tty->fpout, ch)); }
+
+int tty_delg(struct tty *tty,
+            const struct gprintf_ops *gops, void *go,
+            unsigned f, unsigned n)
+  { return (tty->ops->del(tty, gops, go, f, n)); }
+
+int tty_del(struct tty *tty, unsigned f, unsigned n)
+  { return (tty->ops->del(tty, &file_printops, tty->fpout, f, n)); }
+
 /*----- That's all, folks -------------------------------------------------*/
index a82804d8b117df862e1bd177dc127599e7aef03e..e026c15a18de847d565e4512ba1ff4c85dd5d0d9 100644 (file)
--- a/ui/tty.h
+++ b/ui/tty.h
 #  include "bits.h"
 #endif
 
-/*----- Data structures ---------------------------------------------------*/
+#ifndef MLIB_GPRINTF_H
+#  include "gprintf.h"
+#endif
+
+/*----- Attribute flags ---------------------------------------------------*/
 
-/* Attributes. */
+/* Line style attributes. */
 #define TTAF_LNMASK    0x0003u         /* line style mask */
 #define TTAF_LNSHIFT   0               /* line style shift */
 enum {
   TTLN_NONE,                           /*   no line */
   TTLN_ULINE,                          /*   underline */
   TTLN_UULINE,                         /*   double underline */
-  TTLN_STRIKE,                         /*   strikeout */
   TTLN_LIMIT
 };
+
+/* Weight attributes. */
 #define TTAF_WTMASK    0x000cu         /* weight mask */
-#define TTAG_WTSHIFT   2               /* weight shift */
+#define TTAF_WTSHIFT   2               /* weight shift */
 enum {
   TTWT_MED,                            /*   medium */
   TTWT_BOLD,                           /*   bold/bright */
   TTWT_DIM,                            /*   light/dim */
   TTWT_LIMIT
 };
-#define TTAF_ITAL      0x0010u         /* italic/oblique */
-#define TTAF_INVV      0x0020u         /* inverse video */
+
+/* Miscellaneous attributes. */
+#define TTAF_INVV      0x0010u         /* inverse video */
+#define TTAF_STRIKE    0x0020u         /* strike out */
+#define TTAF_ITAL      0x0040u         /* italic/oblique */
+
+/* Colour space attributes. */
 #define TTAF_FGSPCMASK 0x1c00u         /* foreground space mask */
 #define TTAF_FGSPCSHIFT        10              /* foreground space shift */
 #define TTAF_BGSPCMASK 0xe000u         /* background space mask */
@@ -70,133 +80,111 @@ enum {
   TTCSPC_NONE,                         /* no colour */
   TTCSPC_1BPC,                         /* one bit per channel */
   TTCSPC_1BPCBR,                       /* one bit per channel, brighter */
-  TTCSPC_2BPC,                         /* two bits levels per channel */
+  TTCSPC_4LPC,                         /* four levels per channel */
   TTCSPC_8LGS,                         /* eight levels greyscale */
   TTCSPC_6LPC,                         /* six levels per channel */
   TTCSPC_24LGS,                                /* 24 levels greyscale */
-  TTCSPC_8BPC                          /* eight bits per channel */
+  TTCSPC_8BPC,                         /* eight bits per channel */
   TTCSPC_LIMIT
 };
 
-/* Colours. */
-#define TT1BC_RED      1u
-#define TT1BC_GREEN    2u
-#define TT1BC_BLUE     4u
-#define TT1BC_BRIGHT   8u
-#define TTCOL_BLACK    0u
-#define TTCOL_RED      (TT1BC_RED)
-#define TTCOL_GREEN    (TT1BC_GREEN)
-#define TTCOL_YELLOW   (TT1BC_RED | TT1BC_GREEN)
-#define TTCOL_BLUE     (TT1BC_BLUE)
-#define TTCOL_MAGENTA  (TT1BC_RED | TT1BC_BLUE)
-#define TTCOL_CYAN     (TT1BC_GREEN | TT1BC_BLUE)
-#define TTCOL_WHITE    (TT1BC_RED | TT1BC_GREEN | TT1BC_BLUE)
-#define TTCOL_BRBLACK  (TTCOL_BLACK | TT1BC_BRIGHT)
-#define TTCOL_BRRED    (TTCOL_RED | TT1BC_BRIGHT)
-#define TTCOL_BRGREEN  (TTCOL_GREEN | TT1BC_BRIGHT)
-#define TTCOL_BRYELLOW (TTCOL_YELLOW | TT1BC_BRIGHT)
-#define TTCOL_BRBLUE   (TTCOL_BLUE | TT1BC_BRIGHT)
-#define TTCOL_BRMAGENTA        (TTCOL_MAGENTA | TT1BC_BRIGHT)
-#define TTCOL_BRCYAN   (TTCOL_CYAN | TT1BC_BRIGHT)
-#define TTCOL_BRWHITE  (TTCOL_WHITE | TT1BC_BRIGHT)
-
+/*----- Specifying colours ------------------------------------------------*/
+
+/* One-bit-per-channel colours. */
+#define TT1BPC_RED     1u
+#define TT1BPC_GRN     2u
+#define TT1BPC_BLU     4u
+#define TT1BPC_BRI     8u
+#define TTCOL_BLK      0u
+#define TTCOL_RED      (TT1BPC_RED)
+#define TTCOL_GRN      (TT1BPC_GRN)
+#define TTCOL_YLW      (TT1BPC_RED | TT1BPC_GRN)
+#define TTCOL_BLU      (TT1BPC_BLU)
+#define TTCOL_MGN      (TT1BPC_RED | TT1BPC_BLU)
+#define TTCOL_CYN      (TT1BPC_GRN | TT1BPC_BLU)
+#define TTCOL_WHT      (TT1BPC_RED | TT1BPC_GRN | TT1BPC_BLU)
+#define TTCOL_BRBLK    (TTCOL_BLK | TT1BPC_BRI)
+#define TTCOL_BRRED    (TTCOL_RED | TT1BPC_BRI)
+#define TTCOL_BRGRN    (TTCOL_GRN | TT1BPC_BRI)
+#define TTCOL_BRYLW    (TTCOL_YLW | TT1BPC_BRI)
+#define TTCOL_BRBLU    (TTCOL_BLU | TT1BPC_BRI)
+#define TTCOL_BRMGN    (TTCOL_MGN | TT1BPC_BRI)
+#define TTCOL_BRCYN    (TTCOL_CYN | TT1BPC_BRI)
+#define TTCOL_BRWHT    (TTCOL_WHT | TT1BPC_BRI)
+
+/* Two-bits-per-channel colours. */
 #define TTCOL_MK2B(r, g, b) (((r) << 4) | ((g) <<  2) | ((b) <<  0))
 #define TTCOL_2BR(col) (((col) >> 4)&0x03)
 #define TTCOL_2BG(col) (((col) >> 2)&0x03)
 #define TTCOL_2BB(col) (((col) >> 0)&0x03)
 
+/* Six-levels-per-channel colours. */
 #define TTCOL_MK6L(r, g, b) (36*(r) + 6*(g) + (b))
 #define TTCOL_6LR(col) ((col)/36)
 #define TTCOL_6LG(col) (((col)/6)%6)
 #define TTCOL_6LB(col) ((col)%6)
 
-#define TTCOL_MK8B(r, g, b) (((r) << 16) | ((g) <<  8) | ((b) <<  0))
+/* Eight-bits-per-channel colours. */
+#define TTCOL_MK8B(r, g, b)                                            \
+  (((uint32)(r) << 16) | ((g) <<  8) | ((b) <<  0))
 #define TTCOL_8BR(col) (((col) >> 16)&0xff)
 #define TTCOL_8BG(col) (((col) >>  8)&0xff)
 #define TTCOL_8BB(col) (((col) >>  0)&0xff)
 
-struct tty_attr {
-  uint32 f, _res0;                     /* attribute flags, reserved */
-  uint32 fg, bg;                       /* foreground/background colours */
-};
-
-/* Mode settings. */
-#define TTMF_AUTOM     0x00000001u     /* automatic margins */
-#define TTMF_FSCRM     0x00000002u     /* full-screen mode */
-#define TTMF_INS       0x00000004u     /* insert mode */
+/*----- Terminal capabilities ---------------------------------------------*/
 
 /* Attribute capabilities. */
 #define TTACF_ULINE    0x00000001u     /* underline */
 #define TTACF_UULINE   0x00000002u     /* double underline */
-#define TTACF_STRIKE   0x00000004u     /* strikeout */
-#define TTACF_BOLD     0x00000008u     /* bold/bright */
-#define TTACF_DIM      0x00000010u     /* light/dim */
-#define TTACF_ITAL     0x00000020u     /* italic/oblique */
-#define TTACF_INVV     0x00000040u     /* inverse video */
+#define TTACF_BOLD     0x00000004u     /* bold/bright */
+#define TTACF_DIM      0x00000008u     /* light/dim */
+#define TTACF_INVV     0x00000010u     /* inverse video */
+#define TTACF_STRIKE   0x00000020u     /* strikeout */
+#define TTACF_ITAL     0x00000040u     /* italic/oblique */
 #define TTACF_FG       0x00000080u     /* set foreground colour */
 #define TTACF_BG       0x00000100u     /* set background colour */
-#define TTACF_1BPC     0x00000200u     /* one-bit-per-channel space */
-#define TTACF_1BPCBR   0x00000400u    /* one-bit-per-channel bright space */
-#define TTACF_2BPC     0x00000800u     /* two-bits-per-channel space */
-#define TTACF_8LGS     0x00001000u     /* 8-levels-greyscale space */
-#define TTACF_6LPC     0x00002000u     /* six-levels-per-channel space */
-#define TTACF_24LGS    0x00004000u     /* 24-levels-greyscale space */
-#define TTACF_8BPC     0x00008000u     /* eight-bits-per-channel space */
+#define TTACF_1BPC     0x00100000u     /* one-bit-per-channel space */
+#define TTACF_1BPCBR   0x00200000u    /* one-bit-per-channel bright space */
+#define TTACF_4LPC     0x00400000u     /* four-levels-per-channel space */
+#define TTACF_8LGS     0x00800000u     /* 8-levels-greyscale space */
+#define TTACF_6LPC     0x01000000u     /* six-levels-per-channel space */
+#define TTACF_24LGS    0x02000000u     /* 24-levels-greyscale space */
+#define TTACF_8BPC     0x04000000u     /* eight-bits-per-channel space */
+#define TTACF_CSPCMASK 0xfff00000u
+
+/* Mode settings. */
+#define TTMF_AUTOM     0x00000001u     /* automatic margins */
+#define TTMF_FSCRN     0x00000002u     /* full-screen mode */
+#define TTMF_CVIS      0x00000004u     /* visible cursor */
+#define TTMF_INS       0x00000008u     /* insert mode */
+#define TTMF_DEL       0x00000010u     /* delete mode */
 
 /* Other capabilities. */
 #define TTCF_RELMV     0x00000100u     /* relative cursor motion */
 #define TTCF_ABSMV     0x00000200u     /* absolute cursor motion */
 #define TTCF_MIXMV     0x00000400u     /* mixed cursor motion */
-#define TTCF_MVINS     0x00000800u     /* motion preserves insert mode */
-#define TTCF_MVATTR    0x00001000u     /* motion preserves attributes */
-#define TTCF_MMARG     0x00002000u     /* proper magic margins */
-#define TTCF_SCRGN     0x00004000u     /* scroll regions */
-#define TTCF_SCROLL    0x00008000u     /* explicit scrolling */
-#define TTCF_BGER      0x00010000u     /* erasure uses background colour */
-#define TTCF_ERCH      0x00020000u     /* erase characters */
-#define TTCF_ERBOL     0x00040000u     /* erase from beginning of line */
-#define TTCF_EREOL     0x00080000u     /* erase to end of line */
-#define TTCF_ERBOD     0x00100000u     /* erase from beginning of display */
-#define TTCF_EREOD     0x00200000u     /* erase to end of display */
-#define TTCF_ERDSP     0x00400000u     /* erase display */
-#define TTCF_INSCH     0x00800000u     /* insert character */
-#define TTCF_INSLN     0x01000000u     /* insert line */
-#define TTCF_DELCH     0x02000000u     /* delete character */
-#define TTCF_DELLN     0x04000000u     /* delete line */
-
-struct tty_attrlist {
-  uint32 cap_mask, cap_eq;             /* capabilities to select */
-  struct tty_attr attr;                        /* attributes to set */
-};
-#define TTY_ATTRLIST_END { 0, 0, { 0, 0, 0, 0 } }
-
-/* Terminal capability backend libraries. */
-enum {
-  TTLIB_TERMCAP,                       /* traditional `termcap' */
-  TTLIB_TERMINFO,                      /* standard `terminfo' */
-  TTLIB_UNIBILIUM,                     /* `Unibiliium' */
-  TTLIB_XTERM,                      /* assume modern terminal conventions */
-  TTLIB_LIMIT
-};
-
-struct tty_state {
-  uint32 modes;
-  struct tty_attr attr;
-};
-
-struct tty {
-  const struct tty_ops *ops;
-  FILE *fp;
-  unsigned baud, ht, wd;
-  uint32 acaps, ocaps;
-  struct tty_state st;
-};
+#define TTCF_MMARG     0x00000800u     /* proper magic margins */
+#define TTCF_BGER      0x00001000u     /* erasure uses background colour */
+#define TTCF_ERCH      0x00002000u     /* erase characters */
+#define TTCF_ERBOL     0x00004000u     /* erase from beginning of line */
+#define TTCF_EREOL     0x00008000u     /* erase to end of line */
+#define TTCF_ERBOD     0x00010000u     /* erase from beginning of display */
+#define TTCF_EREOD     0x00020000u     /* erase to end of display */
+#define TTCF_ERDSP     0x00040000u     /* erase display */
+#define TTCF_INSCH     0x00080000u     /* insert character */
+#define TTCF_INSLN     0x00100000u     /* insert line */
+#define TTCF_DELCH     0x00200000u     /* delete character */
+#define TTCF_DELLN     0x00400000u     /* delete line */
+
+/*----- Other constants ---------------------------------------------------*/
 
 /* Motion origin. */
 #define TTOF_XHOME 0u                  /* absolute horizontal motion */
 #define TTOF_XCUR 1u                   /* relative horizontal motion */
 #define TTOF_YHOME 0u                  /* absolute vertical motion */
 #define TTOF_YCUR 2u                   /* relative vertical motion */
+#define TTORG_HOME (TTOF_XHOME | TTOF_YHOME)
+#define TTORG_CUR (TTOF_XCUR | TTOF_YCUR)
 
 /* Erasure scope. */
 #define TTEF_LINE 0u                   /* erase within line */
@@ -208,11 +196,54 @@ struct tty {
 #define TTIDF_CH 0u                    /* insert/delete characters */
 #define TTIDF_LN 1u                    /* insert/delete lines */
 
+/* Backend implementations. */
+enum {
+  TTBK_END,                            /* list end marker */
+  TTBK_TERMCAP,                                /* traditional `termcap' */
+  TTBK_TERMINFO,                       /* standard `terminfo' */
+  TTBK_UNIBI,                          /* modern `Unibilium' */
+  TTBK_ANSI,                        /* assume modern terminal conventions */
+  TTBK_LIMIT
+};
+
+/*----- Data structures ---------------------------------------------------*/
+
+/* An attribute setting. */
+struct tty_attr {
+  uint32 f, _res0;                     /* attribute flags, reserved */
+  uint32 fg, bg;                       /* foreground/background colours */
+};
+#define TTY_ATTR_INIT { 0, 0, 0, 0 }
+
+/* A menu of attribute requests. */
+struct tty_attrlist {
+  uint32 cap_mask, cap_eq;             /* capabilities to select */
+  struct tty_attr attr;                        /* attributes to set */
+};
+#define TTY_ATTRLIST_CLEAR { 0, 0, TTY_ATTR_INIT }
+#define TTY_ATTRLIST_END { 0, 1, TTY_ATTR_INIT }
+
+/* A terminal logical state. */
+struct tty_state {
+  uint32 modes;
+  struct tty_attr attr;
+};
+
+/* Terminal control state. */
+struct tty {
+  const struct tty_ops *ops;
+  FILE *fpin, *fpout;
+  unsigned baud, ht, wd, f;
+#define TTF_BORROW 1u
+  uint32 acaps, ocaps;
+  struct tty_state st;
+};
+
 struct tty_ops {
+  void (*release)(struct tty */*tty*/);
   int (*setattr)(struct tty */*tty*/,
                 const struct gprintf_ops */*gops*/, void */*go*/,
-                const struct tty_attr */*old*/,
-                const struct tty_attr */*new*/);
+                const struct tty_attr */*a*/);
   int (*setmodes)(struct tty */*tty*/,
                  const struct gprintf_ops */*gops*/, void */*go*/,
                  uint32 /*modes_bic*/, uint32 /*modes_xor*/);
@@ -231,42 +262,104 @@ struct tty_ops {
   int (*ins)(struct tty */*tty*/,
             const struct gprintf_ops */*gops*/, void */*go*/,
             unsigned /*f*/, unsigned /*n*/);
+  int (*inch)(struct tty */*tty*/,
+             const struct gprintf_ops */*gops*/, void */*go*/,
+             int /*ch*/);
   int (*del)(struct tty */*tty*/,
             const struct gprintf_ops */*gops*/, void */*go*/,
             unsigned /*f*/, unsigned /*n*/);
-  int (*setscrgn)(struct tty */*tty*/,
-                 const struct gprintf_ops */*gops*/, void */*go*/,
-                 unsigned /*y0*/, unsigned /*y1*/);
-  int (*scroll)(struct tty */*tty*/,
-               const struct gprintf_ops */*gops*/, void */*go*/,
-               int /*n*/);
+  int (*_res0)(void), (*_res1)(void), (*_res2)(void), (*_res3)(void);
 };
 
 /*----- Functions provided ------------------------------------------------*/
 
-/* caps
+/* --- @tty_clampattr@ --- *
+ *
+ * Arguments:  @struct tty_attr *a_out@ = selected attributes
+ *             @const struct tty_attr *a@ = requested attributes
+ *             @uint32 acaps@ = terminal's attribute capability mask
+ *
+ * Returns:    ---
  *
- * RA  auto-wrap off
- * SA  auto-wrap on
+ * Use:                Select the closest approximation to the requested attributes
+ *             which can be accommodated by the terminal.
+ */
 
- * AB  ansi background
- * AF  ansi foreground
- * md  bold
- * me  clear text highlighting
- * mr  inverse video
- * op  default colour pair
+extern void tty_clampattr(struct tty_attr */*a_out*/,
+                         const struct tty_attr */*a*/, uint32 /*acaps*/);
 
- * up  cursor up 1 line
 
- * cd  clear to end-of-display
- * ce  clear to end-of-line
 
- * cr  carriage return
- * nw  newline
+extern int tty_resized(struct tty */*tty*/);
 
- * pc  pad character
- */
+#define TTF_OPEN 256u
+extern struct tty *tty_open(FILE */*fp*/, unsigned /*f*/,
+                           const unsigned */*backends*/);
+
+extern void tty_close(struct tty */*tty*/);
+
+extern int tty_setattrg(struct tty */*tty*/,
+                       const struct gprintf_ops */*gops*/, void */*go*/,
+                       const struct tty_attr */*a*/);
+
+extern int tty_setattr(struct tty */*tty*/, const struct tty_attr */*a*/);
+
+extern int tty_setattrlistg(struct tty */*tty*/,
+                           const struct gprintf_ops */*gops*/, void */*go*/,
+                           const struct tty_attrlist */*aa*/);
+
+extern int tty_setattrlist(struct tty */*tty*/,
+                          const struct tty_attrlist */*aa*/);
+
+extern int tty_setmodesg(struct tty */*tty*/,
+                        const struct gprintf_ops */*gops*/, void */*go*/,
+                        uint32 /*modes_bic*/, uint32 /*modes_xor*/);
+
+extern int tty_setmodes(struct tty */*tty*/,
+                       uint32 /*modes_bic*/, uint32 /*modes_xor*/);
+
+extern int tty_moveg(struct tty */*tty*/,
+                    const struct gprintf_ops */*gops*/, void */*go*/,
+                    unsigned /*orig*/, int /*y*/, int /*x*/);
+
+extern int tty_move(struct tty */*tty*/,
+                   unsigned /*orig*/, int /*y*/, int /*x*/);
+
+extern int tty_repeatg(struct tty */*tty*/,
+                      const struct gprintf_ops */*gops*/, void */*go*/,
+                      int /*ch*/, unsigned /*n*/);
+
+extern int tty_repeat(struct tty */*tty*/, int /*ch*/, unsigned /*n*/);
+
+extern int tty_eraseg(struct tty */*tty*/,
+                     const struct gprintf_ops */*gops*/, void */*go*/,
+                     unsigned /*f*/);
+
+extern int tty_erase(struct tty */*tty*/, unsigned /*f*/);
+
+extern int tty_erchg(struct tty */*tty*/,
+                    const struct gprintf_ops */*gops*/, void */*go*/,
+                    unsigned /*n*/);
+
+extern int tty_erch(struct tty */*tty*/, unsigned /*n*/);
+
+extern int tty_insg(struct tty */*tty*/,
+                   const struct gprintf_ops */*gops*/, void */*go*/,
+                   unsigned /*f*/, unsigned /*n*/);
+
+extern int tty_ins(struct tty */*tty*/, unsigned /*f*/, unsigned /*n*/);
+
+extern int tty_inchg(struct tty */*tty*/,
+                    const struct gprintf_ops */*gops*/, void */*go*/,
+                    int /*ch*/);
+
+extern int tty_inch(struct tty */*tty*/, int /*ch*/);
+
+extern int tty_delg(struct tty */*tty*/,
+                   const struct gprintf_ops */*gops*/, void */*go*/,
+                   unsigned /*f*/, unsigned /*n*/);
 
+extern int tty_del(struct tty */*tty*/, unsigned /*f*/, unsigned /*n*/);
 
 /*----- That's all, folks -------------------------------------------------*/
 
index 7e70a3b45106049355e4a19255c70546a0c92580..d2658e70cbadcb29f0e7cffd3142063ac0205671 100644 (file)
 
 #include "macros.h"
 #include "report.h"
+#include "tty.h"
 #include "ttycolour.h"
 
 /*----- Main code ---------------------------------------------------------*/
 
-int ttycolour_config(ttycolour_attr *attr, const char *user, unsigned f,
-                    const struct ttycolour_style *tab)
+/* --- @env_setting_p@ --- *
+ *
+ * Arguments:  @const char *var@ = environment variable name
+ *
+ * Returns:    Nonzero if the variable is set to a non-empty value,
+ *             otherwise zero.
+ *
+ * Use:                This is the recommended way to check the `NO_COLOR',
+ *             `CLICOLOR' and `CLICOLOR_FORCE' variables.
+ */
+
+static int env_setting_p(const char *var)
+  { const char *p; p = getenv(var); return (p && *p); }
+
+/* --- @ttycolour_enablep@ --- *
+ *
+ * Arguments:  @unsigned f@ = flags
+ *
+ * Returns:    Nonzero if colours should be applied to output, otherwise
+ *             zero.
+ *
+ * Use:                This function determines whether it's generally a good idea
+ *             to produce output in pretty colours.  Set @TCEF_TTY@ if the
+ *             output stream is -- or should be considered to be --
+ *             interactive (e.g., according to @isatty@); set @TCEF_DFLT@ if
+ *             the application prefers to produce coloured output if
+ *             possible.
+ *
+ *             The detailed behaviour is as follows.  (Since the purpose of
+ *             this function is to abide by common conventions and to be
+ *             convenient for users, these details may change in future.)
+ *
+ *               * If the `NO_COLOR' environment variable is non-empty, then
+ *                 colour is disabled (%%\url{https://no-color.org/}%%).
+ *
+ *               * If the `TERM' variable is set to `dumb', then colour is
+ *                 disabled (Emacs).
+ *
+ *               * If the `FORCE_COLOR' environment variable is non-empty,
+ *                 then colour is enabled, unless the value is 0, in which
+ *                 case colour is disabled (apparently from the Node
+ *                 community, %%\url{%%https://force-color.org/}%% and
+ *                 %%\url{https://nodejs.org/api/tty.html#writestreamgetcolordepthenv}%%).
+ *
+ *               * If the `CLICOLOR_FORCE' environment variable is
+ *                 non-empty, then colour is enabled (apparently from
+ *                 Mac OS, (%%\url{http://bixense.com/clicolors/}%%).
+ *
+ *               * If the @TCEF_TTY@ flag is clear, then colour is disabled.
+ *
+ *               * If the @TCEF_DFLT@ flag is set, then colour is enabled.
+ *
+ *               * If the `CLICOLOR' environment variable is non-empty, then
+ *                 colour is enabled (again, apparently from Mac OS,
+ *                 (%%\url{http://bixense.com/clicolors/}%%).
+ *
+ *               * Otherwise, colour is disabled.
+ */
+
+int ttycolour_enablep(unsigned f)
+{
+  const char *t;
+
+  if (env_setting_p("NO_COLOR")) return (0);
+  else if (t = getenv("TERM"), !t || STRCMP(t, ==, "dumb")) return (0);
+  else if (t = getenv("FORCE_COLOR"), t && *t) return (*t == '0' ? 0 : 1);
+  else if (env_setting_p("CLICOLOR_FORCE")) return (1);
+  else if (!(f&TCEF_TTY)) return (0);
+  else if ((f&TCEF_DFLT) || env_setting_p("CLICOLOR")) return (1);
+  else return (0);
+}
+
+
+
+int ttycolour_config(struct tty_attr *attr, const char *user, unsigned f,
+                    struct tty *tty, const struct ttycolour_style *tab)
 {
   const char *p, *q;
+  const struct tty_attrlist *aa;
   size_t n;
-  unsigned i, arg, a;
+  unsigned i, arg, a, fg, bg, st, spc, clr;
   int rc = 0;
 
-  for (i = 0; tab[i].tok; i++) attr[i] = tab[i].dflt;
+#define ST_BASE 0u
+#define ST_EXT 1u
+#define ST_CLR 2u
+#define ST_RED 3u
+#define ST_GREEN 4u
+#define ST_BLUE 5u
+#define ST_MASK 0x7fu
+#define ST_FG 0x00u
+#define ST_BG 0x80u
+
+#define SETAFIELD(f, v) (a = (a&~TTAF_##f##MASK) | ((v) << TTAF_##f##SHIFT))
+
+  for (i = 0; tab[i].tok; i++) {
+    aa = tab[i].dflt;
+    if (aa) {
+      for (;; aa++)
+       if ((tty->acaps&aa->cap_mask) == aa->cap_eq)
+         { attr[i] = aa->attr; goto next_token; }
+       else if (!aa->cap_mask)
+         break;
+    }
+    attr[i].f = attr[i]._res0 = attr[i].fg = attr[i].bg = 0;
+  next_token:;
+  }
 
   if (f&TCIF_GETENV) p = getenv(user);
   else p = user;
+
   if (p)
     while (*p) {
       n = strcspn(p, "=:"); q = p + n;
@@ -63,40 +163,134 @@ int ttycolour_config(ttycolour_attr *attr, const char *user, unsigned f,
       rc = -1; q = p + strcspn(p, ":"); p = *q ? q + 1 : q; continue;
 
     found_tok:
-      a = 0; arg = 0; q++;
+      a = fg = bg = 0; st = ST_BASE; arg = 0; clr = 0; q++;
       for (;;) {
 
        if (ISDIGIT(*q))
          { arg = 10*arg + (*q++ - '0'); continue; }
 
-       switch (arg) {
-         case 0: a = 0; break;
-         case 1: a |= TCAF_BOLD; break;
-         case 39: a &= ~(TCAF_FG | TCAF_FGMASK); break;
-         case 49: a &= ~(TCAF_BG | TCAF_BGMASK); break;
-         case 30: case 31: case 32: case 33:
-         case 34: case 35: case 36: case 37:
-           a = (a&~TCAF_FGMASK) | TCAF_FG | ((arg - 30) << TCAF_FGSHIFT);
+       switch (st&ST_MASK) {
+         case ST_BASE:
+           switch (arg) {
+             case 0: a = fg = bg = 0; break;
+             case 1: SETAFIELD(WT, TTWT_BOLD); break;
+             case 2: SETAFIELD(WT, TTWT_DIM); break;
+             case 3: a |= TTAF_ITAL; break;
+             case 4: SETAFIELD(LN, TTLN_ULINE); break;
+             case 7: a |= TTAF_INVV; break;
+             case 21: SETAFIELD(LN, TTLN_UULINE); break;
+             case 22: a &= ~TTAF_WTMASK; break;
+             case 23: a &= ~TTAF_ITAL; break;
+             case 24: a &= ~TTAF_LNMASK; break;
+             case 27: a &= ~TTAF_INVV; break;
+
+             case 30: case 31: case 32: case 33:
+             case 34: case 35: case 36: case 37:
+               SETAFIELD(FGSPC, TTCSPC_1BPC);
+               fg = arg - 30;
+               break;
+             case 90: case 91: case 92: case 93:
+             case 94: case 95: case 96: case 97:
+               SETAFIELD(FGSPC, TTCSPC_1BPCBR);
+               fg = (arg - 90) | TT1BPC_BRI;
+               break;
+             case 38: st = ST_EXT | ST_FG; break;
+             case 39: SETAFIELD(FGSPC, TTCSPC_NONE); break;
+
+             case 40: case 41: case 42: case 43:
+             case 44: case 45: case 46: case 47:
+               SETAFIELD(BGSPC, TTCSPC_1BPC);
+               bg = arg - 40;
+               break;
+             case 100: case 101: case 102: case 103:
+             case 104: case 105: case 106: case 107:
+               SETAFIELD(BGSPC, TTCSPC_1BPCBR);
+               fg = (arg - 100) | TT1BPC_BRI;
+               break;
+             case 48: st = ST_EXT | ST_BG; break;
+             case 49: SETAFIELD(BGSPC, TTCSPC_NONE); break;
+
+             default:
+               if (f&TCIF_REPORT)
+                 moan("unknown colour code %u in `%.*s' string in `%s'",
+                      arg, (int)n, p, user);
+               rc = -1; break;
+           }
            break;
-         case 40: case 41: case 42: case 43:
-         case 44: case 45: case 46: case 47:
-           a = (a&~TCAF_BGMASK) | TCAF_BG | ((arg - 40) << TCAF_BGSHIFT);
+
+         case ST_EXT:
+           switch (arg) {
+             case 2: st = (st&~ST_MASK) | ST_RED; break;
+             case 5: st = (st&~ST_MASK) | ST_CLR; break;
+             default:
+               if (f&TCIF_REPORT)
+                 moan("unknown extended colour space %u "
+                      "in `%.*s' string in `%s'",
+                      arg, (int)n, p, user);
+               st = ST_BASE; rc = -1; break;
+           }
            break;
-         case 90: case 91: case 92: case 93:
-         case 94: case 95: case 96: case 97:
-           a = (a&~TCAF_FGMASK) | TCAF_FG |
-               (((arg - 90) | TCCF_BRIGHT) << TCAF_FGSHIFT);
+
+         case ST_CLR:
+           if (arg < 8)
+             { spc = TTCSPC_1BPC; clr = arg; }
+           else if (arg < 16)
+             { spc = TTCSPC_1BPCBR; clr = (arg - 8) | TT1BPC_BRI; }
+           else if (arg < 232)
+             { spc = TTCSPC_6LPC; clr = arg - 16; }
+           else if (arg < 256)
+             { spc = TTCSPC_24LGS; clr = arg - 232; }
+           else {
+             if (f&TCIF_REPORT)
+               moan("indexed colour %u out of range "
+                    "in `%.*s' string in `%s'",
+                    arg, (int)n, p, user);
+             st = ST_BASE; rc = -1; break;
+           }
+           if (st&ST_BG) { SETAFIELD(BGSPC, spc); bg = clr; }
+           else { SETAFIELD(FGSPC, spc); fg = clr; }
+           st = ST_BASE; break;
+
+         case ST_RED:
+           if (arg < 256)
+             { clr = U32(arg) << 16; st = ST_GREEN; }
+           else {
+             if (f&TCIF_REPORT)
+               moan("red channel %u out of range "
+                    "in `%.*s' string in `%s'",
+                    arg, (int)n, p, user);
+             st = ST_BASE; rc = -1;
+           }
            break;
-         case 100: case 101: case 102: case 103:
-         case 104: case 105: case 106: case 107:
-           a = (a&~TCAF_BGMASK) | TCAF_BG |
-               (((arg - 100) | TCCF_BRIGHT) << TCAF_BGSHIFT);
+
+         case ST_GREEN:
+           if (arg < 256)
+             { clr |= U32(arg) << 8; st = ST_BLUE; }
+           else {
+             if (f&TCIF_REPORT)
+               moan("green channel %u out of range "
+                    "in `%.*s' string in `%s'",
+                    arg, (int)n, p, user);
+             st = ST_BASE; rc = -1; break;
+           }
            break;
+
+         case ST_BLUE:
+           if (arg < 256) {
+             clr |= U32(arg) << 0;
+             if (st&ST_BG) { SETAFIELD(BGSPC, TTCSPC_8BPC); bg = clr; }
+             else { SETAFIELD(FGSPC, TTCSPC_8BPC); fg = clr; }
+           } else {
+             if (f&TCIF_REPORT)
+               moan("blue channel %u out of range "
+                    "in `%.*s' string in `%s'",
+                    arg, (int)n, p, user);
+             rc = -1; break;
+           }
+           st = ST_BASE; break;
+
          default:
-           if (f&TCIF_REPORT)
-             moan("unknown colour code %u in `%.*s' string in `%s'",
-                  arg, (int)n, p, user);
-           rc = -1; break;
+           assert(0);
        }
        arg = 0;
        if (!*q || *q == ':') break;
@@ -108,89 +302,11 @@ int ttycolour_config(ttycolour_attr *attr, const char *user, unsigned f,
        }
        q++;
       }
-      attr[i] = a; p = *q ? q + 1 : q;
+      attr[i].f = a; attr[i]._res0 = 0; attr[i].fg = fg; attr[i].bg = bg;
+      p = *q ? q + 1 : q;
     }
 
   return (rc);
 }
 
-void ttycolour_init(struct ttycolour_state *tc)
-  { tc->attr = 0; }
-
-/* --- @ttycolour_setattr@ --- *
- *
- * Arguments:  @const struct gprintf_ops *ops@ = formatting operations
- *             @void *go@ = output sink
- *             @struct ttycolour_state *tc
- *             @ttycolour_attr attr@ = attribute code to set
- *
- * Returns:    Zero on success, %$-1$% on error.
- *
- * Use:                Send a control sequence to the output stream so that
- *             subsequent text is printed with the given attributes.
- *
- *             Some effort is taken to avoid unnecessary control sequences.
- *             In particular, if @attr@ matches the current terminal
- *             settings already, then nothing is written.
- */
-
-int ttycolour_setattr(const struct gprintf_ops *gops, void *go,
-                     struct ttycolour_state *tc, ttycolour_attr attr)
-{
-  unsigned diff = tc->attr ^ attr;
-  unsigned f = 0;
-
-#define f_semi 1u
-
-#define PUT_SEMI do {                                                  \
-  if (!(f&f_semi)) f |= f_semi;                                                \
-  else if (gops->putch(go, ';') < 0) return (-1);                      \
-} while (0)
-
-#define SET_COLOUR(norm, bright, colour) do {                          \
-  unsigned char _col = (colour);                                       \
-                                                                       \
-  PUT_SEMI;                                                            \
-  gprintf(gops, go, "%d",                                              \
-         (_col&TCCF_BRIGHT ? (bright) : (norm)) + (_col&TCCF_RGBMASK)); \
-} while (0)
-
-  /* If there's nothing to do, we might as well stop now. */
-  if (!diff) return (0);
-
-  /* Start on the control command. */
-  if (gops->putm(go, "\x1b[", 2) < 0) return (-1);
-
-  /* Change the boldness if necessary. */
-  if (diff&TCAF_BOLD) {
-    PUT_SEMI;
-    if (attr&TCAF_BOLD) { if (gops->putch(go, '1') < 0) return (-1); }
-    else { diff = tc->attr; if (gops->putch(go, '0') < 0) return (-1); }
-  }
-
-  /* Change the foreground colour if necessary. */
-  if (diff&(TCAF_FG | TCAF_FGMASK)) {
-    if (attr&TCAF_FG)
-      SET_COLOUR(30, 90, (attr&TCAF_FGMASK) >> TCAF_FGSHIFT);
-    else
-      { PUT_SEMI; if (gops->putm(go, "39", 2) < 0) return (-1); }
-  }
-
-  /* Change the background colour if necessary. */
-  if (diff&(TCAF_BG | TCAF_BGMASK)) {
-    if (attr&TCAF_BG)
-      SET_COLOUR(40, 100, (attr&TCAF_BGMASK) >> TCAF_BGSHIFT);
-    else
-      { PUT_SEMI; if (gops->putm(go, "49", 2) < 0) return (-1); }
-  }
-
-  /* Terminate the control command and save the new attributes. */
-  if (gops->putch(go, 'm') < 0) return (-1);
-  tc->attr = attr; return (0);
-
-#undef f_semi
-#undef PUT_SEMI
-#undef SET_COLOUR
-}
-
 /*----- That's all, folks -------------------------------------------------*/
index 4fb8472cbc54ef9f2c8ec3a8e61e3a39425265b1..535248f560a28f52d00d856cd917e04227b691b4 100644 (file)
 #  include "macros.h"
 #endif
 
-/*----- Functions provided ------------------------------------------------*/
+#ifndef MLIB_TTY_H
+#  include "tty.h"
+#endif
 
-/* Attributes for colour output.
- *
- * An attribute word holds a foreground colour in the low nibble, a
- * background colour in the next nibble, and some flags in the next few bits.
- * A colour is expressed in classic 1-bit-per-channel style, with red, green,
- * and blue in bits 0, 1, and 2, and a `bright' flag in bit 3.
- */
-typedef unsigned short ttycolour_attr; /* type of an attribute */
-
-#define TCAF_FGMASK 0x0f               /* foreground colour mask */
-#define TCAF_FGSHIFT 0                 /* foreground colour shift */
-#define TCAF_BGMASK 0xf0               /* background colour mask */
-#define TCAF_BGSHIFT 4                 /* background colour shift */
-#define TCAF_FG 256u                   /* set foreground? */
-#define TCAF_BG 512u                   /* set background? */
-#define TCAF_BOLD 1024u                        /* set bold? */
-
-#define TCCF_RED 1u                    /* red channel */
-#define TCCF_GREEN 2u                  /* green channel */
-#define TCCF_BLUE 4u                   /* blue channel */
-#define TCCF_RGBMASK (TCCF_RED | TCCF_GREEN | TCCF_BLUE)
-#define TCCF_BRIGHT 8u                 /* bright colour flag */
-
-#define TTYCOL_BLACK 0u                        /* colour codes... */
-#define TTYCOL_RED (TCCF_RED)
-#define TTYCOL_GREEN (TCCF_GREEN)
-#define TTYCOL_YELLOW (TCCF_RED | TCCF_GREEN)
-#define TTYCOL_BLUE (TCCF_BLUE)
-#define TTYCOL_MAGENTA (TCCF_RED | TCCF_BLUE)
-#define TTYCOL_CYAN (TCCF_GREEN | TCCF_BLUE)
-#define TTYCOL_WHITE (TCCF_RED | TCCF_GREEN | TCCF_BLUE)
-
-#define TTYCOL_BRBLACK (TTYCOL_BLACK | TCCF_BRIGHT)
-#define TTYCOL_BRRED (TTYCOL_RED | TCCF_BRIGHT)
-#define TTYCOL_BRGREEN (TTYCOL_GREEN | TCCF_BRIGHT)
-#define TTYCOL_BRYELLOW (TTYCOL_YELLOW | TCCF_BRIGHT)
-#define TTYCOL_BRBLUE (TTYCOL_BLUE | TCCF_BRIGHT)
-#define TTYCOL_BRMAGENTA (TTYCOL_MAGENTA | TCCF_BRIGHT)
-#define TTYCOL_BRCYAN (TTYCOL_CYAN | TCCF_BRIGHT)
-#define TTYCOL_BRWHITE (TTYCOL_WHITE | TCCF_BRIGHT)
-
-#define TC_FG(col) (TCAF_FG | (TTYCOL_##col) << TCAF_FGSHIFT) /* set fg */
-#define TC_BG(col) (TCAF_BG | (TTYCOL_##col) << TCAF_BGSHIFT) /* set bg */
+/*----- Functions provided ------------------------------------------------*/
 
 struct ttycolour_style {
   /* Table of style tokens and their defaults.  The table is terminated with
@@ -94,16 +54,9 @@ struct ttycolour_style {
    */
 
   const char *tok;                     /* style token name */
-  ttycolour_attr dflt;                 /* default attribute value */
+  const struct tty_attrlist *dflt;     /* default attribute value */
 };
 
-struct ttycolour_state {
-  /* State maintained by @ttycolour_setattr@. */
-
-  ttycolour_attr attr;                 /* current attribute value */
-};
-#define TTYCOLOUR_STATE_INIT { 0 }     /* initializer for state */
-
 /* Machinery for building the constants and tables.
  *
  * The caller should define the styles it supports in a macro taking two
@@ -150,6 +103,55 @@ struct ttycolour_state {
   { 0, 0 }                                                             \
 }
 
+/* --- @ttycolour_enablep@ --- *
+ *
+ * Arguments:  @unsigned f@ = flags
+ *
+ * Returns:    Nonzero if colours should be applied to output, otherwise
+ *             zero.
+ *
+ * Use:                This function determines whether it's generally a good idea
+ *             to produce output in pretty colours.  Set @TCEF_TTY@ if the
+ *             output stream is -- or should be considered to be --
+ *             interactive (e.g., according to @isatty@); set @TCEF_DFLT@ if
+ *             the application prefers to produce coloured output if
+ *             possible.
+ *
+ *             The detailed behaviour is as follows.  (Since the purpose of
+ *             this function is to abide by common conventions and to be
+ *             convenient for users, these details may change in future.)
+ *
+ *               * If the `NO_COLOR' environment variable is non-empty, then
+ *                 colour is disabled (%%\url{https://no-color.org/}%%).
+ *
+ *               * If the `TERM' variable is set to `dumb', then colour is
+ *                 disabled (Emacs).
+ *
+ *               * If the `FORCE_COLOR' environment variable is non-empty,
+ *                 then colour is enabled, unless the value is 0, in which
+ *                 case colour is disabled (apparently from the Node
+ *                 community, %%\url{%%https://force-color.org/}%% and
+ *                 %%\url{https://nodejs.org/api/tty.html#writestreamgetcolordepthenv}%%).
+ *
+ *               * If the `CLICOLOR_FORCE' environment variable is
+ *                 non-empty, then colour is enabled (apparently from
+ *                 Mac OS, (%%\url{http://bixense.com/clicolors/}%%).
+ *
+ *               * If the @TCEF_TTY@ flag is clear, then colour is disabled.
+ *
+ *               * If the @TCEF_DFLT@ flag is set, then colour is enabled.
+ *
+ *               * If the `CLICOLOR' environment variable is non-empty, then
+ *                 colour is enabled (again, apparently from Mac OS,
+ *                 (%%\url{http://bixense.com/clicolors/}%%).
+ *
+ *               * Otherwise, colour is disabled.
+ */
+
+#define TCEF_TTY 1u                    /*   output is interactive */
+#define TCEF_DFLT 2u                   /*   turn on by default */
+extern int ttycolour_enablep(unsigned /*f*/);
+
 /* --- @ttycolour_config@ --- *
  *
  * Arguments:  @ttycolour_attr *attr@ = pointer to attribute table
@@ -174,16 +176,11 @@ struct ttycolour_state {
 
 #define TCIF_GETENV 1u
 #define TCIF_REPORT 2u
-extern int ttycolour_config(ttycolour_attr */*attr*/,
+extern int ttycolour_config(struct tty_attr */*attr*/,
                            const char */*user*/, unsigned /*f*/,
+                           struct tty */*tty*/,
                            const struct ttycolour_style */*tab*/);
 
-extern void ttycolour_init(struct ttycolour_state */*tc*/);
-
-extern int ttycolour_setattr
-  (const struct gprintf_ops */*gops*/, void */*go*/,
-   struct ttycolour_state */*tc*/, ttycolour_attr /*attr*/);
-
 /*----- That's all, folks -------------------------------------------------*/
 
 #ifdef __cplusplus
diff --git a/ui/ttyprogress.c b/ui/ttyprogress.c
new file mode 100644 (file)
index 0000000..dc023d5
--- /dev/null
@@ -0,0 +1,626 @@
+/* -*-c-*-
+ *
+ * Progress bars for terminal programs
+ *
+ * (c) 2025 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the mLib utilities library.
+ *
+ * mLib is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * mLib is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with mLib.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#define _XOPEN_SOURCE
+#include "config.h"
+
+#include <assert.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+
+#include "alloc.h"
+#include "arena.h"
+#include "dstr.h"
+#include "gprintf.h"
+#include "growbuf.h"
+#include "tty.h"
+#include "ttycolour.h"
+#include "ttyprogress.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+int ttyprogress_init(struct ttyprogress *progress, struct tty *tty)
+{
+#define GENATTR(want, attrs, fgspc, fgcol, bgspc, bgcol)               \
+  { (want), (want),                                                    \
+    { ((fgspc) << TTAF_FGSPCSHIFT) |                                   \
+      ((bgspc) << TTAF_BGSPCSHIFT) | (attrs),                          \
+      0, (fgcol), (bgcol) } }
+
+#define FGBG(want, attrs, fgcol, bgcol)                                        \
+  GENATTR(want | TTACF_FG | TTACF_BG, attrs,                           \
+         TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_1BPCBR, TTCOL_##bgcol)
+#define ATTR(want, attrs)                                              \
+  GENATTR(want, attrs, TTCSPC_NONE, 0, TTCSPC_NONE, 0)
+
+#define BOLD (TTWT_BOLD << TTAF_WTSHIFT)
+
+  static const struct tty_attrlist
+    bbar_attrs[] = { FGBG(0, 0, BLK, GRN), ATTR(0, TTAF_INVV) },
+    ebar_attrs[] = { FGBG(0, 0, BLK, YLW), TTY_ATTRLIST_CLEAR },
+    note_attrs[] = { FGBG(0, BOLD, WHT, BLU), ATTR(0, TTAF_INVV | BOLD) },
+    warn_attrs[] = { FGBG(0, BOLD, WHT, MGN), ATTR(0, TTAF_INVV | BOLD) },
+    err_attrs[] = { FGBG(0, BOLD, WHT, RED), ATTR(0, TTAF_INVV | BOLD) };
+  static const struct ttycolour_style hltab[] =
+    TTYCOLOUR_INITTAB(TTYPROGRESS_HIGHLIGHTS);
+
+#undef GENATTR
+#undef FGBG
+#undef ATTR
+#undef BOLD
+
+  /* Clear the progress state. */
+  progress->tty = tty;
+  progress->items = progress->end_item = 0;
+  progress->nitems = 0; progress->last_lines = 0;
+  progress->line.a = arena_global; dstr_create(&progress->line.t);
+  progress->line.p = 0; progress->line.sz = 0;
+  progress->tv_update.tv_sec = 0; progress->tv_update.tv_usec = 0;
+
+  /* Check that the terminal is sufficiently cromulent. */
+  if (!tty || !(tty->ocaps&TTCF_RELMV) || !(tty->ocaps&TTCF_EREOD))
+    { progress->tty = 0; return (-1); }
+
+  /* Configure the highlight attributes. */
+  ttycolour_config(progress->attr,
+                  "MLIB_TTYPROGRESS_COLOURS", TCIF_GETENV, tty, hltab);
+
+  /* All done. */
+  return (0);
+}
+
+void ttyprogress_free(struct ttyprogress *progress)
+{
+  dstr_destroy(&progress->line.t);
+  x_free(progress->line.a, progress->line.p);
+}
+
+/*----- Active item list maintenance --------------------------------------*/
+
+int ttyprogress_additem(struct ttyprogress *progress,
+                    struct ttyprogress_item *item)
+{
+  if (item->parent) return (-1);
+  item->prev = progress->end_item; item->next = 0;
+  if (progress->end_item) progress->end_item->next = item;
+  else progress->items = item;
+  progress->end_item = item; item->parent = progress;
+  progress->nitems++;
+
+  return (0);
+}
+
+int ttyprogress_removeitem(struct ttyprogress *progress,
+                       struct ttyprogress_item *item)
+{
+  if (!item->parent) return (-1);
+  if (item->next) item->next->prev = item->prev;
+  else (progress->end_item) = item->prev;
+  if (item->prev) item->prev->next = item->next;
+  else (progress->items) = item->next;
+  progress->nitems--; item->parent = 0;
+
+  return (0);
+}
+
+/*----- Render state lifecycle --------------------------------------------*/
+
+static void setup_render_state(struct ttyprogress *progress,
+                              struct ttyprogress_render *render)
+{
+  struct tty *tty = progress->tty;
+
+  /* Clear everything. */
+  render->tty = tty;
+
+  /* Update the current terminal size. */
+  tty_resized(tty);
+
+  /* We'll render progress bars with colour or standout if we can; otherwise,
+   * we'll just insert a `|' in the right place, but that takes up an extra
+   * column, so deduct one from the terminal's width to compensate.  Deduct
+   * another one if the terminal doesn't let us leave the cursor in the final
+   * column without scrolling or wrapping.
+   */
+  render->width = tty->wd;
+  if (render->width && !(tty->acaps&(TTCF_MMARG | TTMF_AUTOM)))
+    render->width--;
+  if (render->width && !(tty->acaps&(TTACF_BG | TTACF_INVV)))
+    render->width--;
+
+  /* Borrow the line buffer and highlight table from the master state. */
+  render->line = &progress->line;
+  render->attr = progress->attr;
+}
+
+/*----- Measuring string widths -------------------------------------------*/
+
+#if defined(HAVE_MBRTOWC) && defined(HAVE_WCWIDTH)
+
+#include <wchar.h>
+
+#define CONV_MORE ((size_t)-2)
+#define CONV_BAD ((size_t)-1)
+
+struct measure {
+  mbstate_t ps;                                /* conversion state */
+  const char *p; size_t i, sz;         /* input string, and cursor */
+  unsigned wd;                         /* width accumulated so far */
+};
+
+static void init_measure(struct measure *m, const char *p, size_t sz)
+       /* Set up M to measure the SZ-byte string P. */
+{
+  m->p = p; m->sz = sz; m->i = 0; m->wd = 0;
+  memset(&m->ps, 0, sizeof(m->ps));
+}
+
+static int advance_measure(struct measure *m)
+       /* Advance the measurement in M by one character.  Return zero if the
+        * end of the string has been reached, or nonzero if there is more to
+        * come.
+        */
+{
+  wchar_t wch;
+  unsigned chwd;
+  size_t n;
+
+  /* Determine the next character's code WCH, the length N of its encoding in
+   * P in bytes, and the character's width CHWD in columns.
+   */
+  n = mbrtowc(&wch, m->p + m->i, m->sz - m->i, &m->ps);
+  if (!n) { chwd = 0; n = m->sz - m->i; }
+  else if (n == CONV_MORE) { chwd = 2; n = m->sz - m->i; }
+  else if (n == CONV_BAD) { chwd = 2; n = 1; }
+  else chwd = wcwidth(wch);
+
+  /* Advance the state. */
+  m->i += n; m->wd += chwd;
+
+  /* Report whether there's more to come. */
+  return (m->i < m->sz);
+}
+
+static unsigned string_width(const char *p, size_t sz)
+       /* Return the width of the SZ-byte string P, in terminal columns. */
+{
+  struct measure m;
+
+  init_measure(&m, p, sz);
+  while (advance_measure(&m));
+  return (m.wd);
+}
+
+static size_t split_string(const char *p, size_t sz,
+                          unsigned *wd_out, unsigned maxwd)
+       /* Return the size, in bytes, of the shortest prefix of the SZ-byte
+        * string P which is no less than MAXWD columns wide, or SZ if it's
+        * just too short.  Store the actual width in *WD_OUT.
+        */
+{
+  struct measure m;
+  size_t i; unsigned wd;
+  int more;
+
+  init_measure(&m, p, sz);
+
+  /* Advance until we're past the bound. */
+  for (;;) {
+    if (!advance_measure(&m)) { *wd_out = m.wd; return (sz); }
+    if (m.wd >= maxwd) break;
+  }
+
+  /* Now /continue/ advancing past zero-width characters until we find
+   * something that wasn't zero-width.  These might be combining accents or
+   * somesuch, and leaving them off would definitely be wrong.
+   */
+  wd = m.wd; i = m.i;
+  for (;;) {
+    more = advance_measure(&m);
+    if (m.wd > wd) break;
+    i = m.i;
+    if (!more) break;
+  }
+
+  /* All done. */
+  *wd_out = wd; return (i);
+}
+
+#else
+
+static unsigned string_width(const char *p, size_t sz) { return (sz); }
+
+static size_t split_string(const char *p, size_t sz,
+                          unsigned *wd_out, unsigned maxwd)
+{
+  unsigned wd;
+
+  if (sz <= maxwd) wd = sz;
+  else wd = maxwd;
+  *wd_out = wd; return (wd);
+}
+
+#endif
+
+/*----- Output buffer handling --------------------------------------------*/
+
+static void grow_linebuf(struct ttyprogress_render *render, size_t want)
+       /* Extend the line buffer in RENDER so that it's at least WANT bytes
+        * long.  Shuffle the accumulated left and right material in the
+        * buffer as necessary.
+        */
+{
+  struct ttyprogress_buffer *line = render->line;
+  char *newbuf; size_t newsz;
+
+  /* Return if there's already enough space. */
+  if (want <= line->sz) return;
+
+  /* Work out how much space to allocate.  The initial size is a rough guess
+   * based on the size of UTF-8 encoded characters, though it's not an upper
+   * bound because many characters have zero width.  Double the buffer size
+   * if it's too small.  Sneakily insert a terminating zero byte just in
+   * case.
+   */
+  newsz = line->sz;
+  GROWBUF_SIZE(size_t, newsz, want, 4*render->width + 1, 1);
+  newbuf = x_alloc(line->a, newsz + 1);
+  newbuf[newsz] = 0;
+
+  /* Copy the left and right strings into the new buffer. */
+  if (render->leftsz)
+    memcpy(newbuf, line->p, render->leftsz);
+  if (render->rightsz)
+    memcpy(newbuf + newsz - render->rightsz,
+          line->p + line->sz - render->rightsz,
+          render->rightsz);
+
+  /* Free the old buffer and remember the new one. */
+  x_free(line->a, line->p); line->p = newbuf; line->sz = newsz;
+}
+
+enum { LEFT, RIGHT, STOP };
+
+static int putstr(struct ttyprogress_render *render, unsigned side,
+                 const char *p, size_t n)
+       /* Add the N-byte string P to SIDE of the line buffer in RENDER.
+        * Return 0 on success or -1 if this fails for any reason.
+        */
+{
+  unsigned newwd = string_width(p, n);
+  size_t want;
+
+  if (newwd >= render->width - render->leftwd - render->rightwd) return (-1);
+  want = render->leftsz + render->rightsz + n;
+  if (want > render->line->sz) grow_linebuf(render, want);
+  switch (side) {
+    case LEFT:
+      memcpy(render->line->p + render->leftsz, p, n);
+      render->leftsz += n; render->leftwd += newwd;
+      break;
+    case RIGHT:
+      memcpy(render->line->p + render->line->sz - render->rightsz - n, p, n);
+      render->rightsz += n; render->rightwd += newwd;
+      break;
+    default:
+      assert(0);
+  }
+  return (0);
+}
+
+int ttyprogress_vputleft(struct ttyprogress_render *render,
+                     const char *fmt, va_list *ap)
+{
+  dstr *t = &render->line->t;
+
+  DRESET(t); dstr_vputf(t, fmt, ap);
+  return (putstr(render, LEFT, t->buf, t->len));
+}
+
+int ttyprogress_vputright(struct ttyprogress_render *render,
+                      const char *fmt, va_list *ap)
+{
+  dstr *t = &render->line->t;
+
+  DRESET(t); dstr_vputf(t, fmt, ap);
+  return (putstr(render, RIGHT, t->buf, t->len));
+}
+
+int ttyprogress_putleft(struct ttyprogress_render *render,
+                       const char *fmt, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, fmt); rc = ttyprogress_vputleft(render, fmt, &ap); va_end(ap);
+  return (rc);
+}
+
+int ttyprogress_putright(struct ttyprogress_render *render,
+                     const char *fmt, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, fmt); rc = ttyprogress_vputright(render, fmt, &ap); va_end(ap);
+  return (rc);
+}
+
+/*----- Maintaining the progress display ----------------------------------*/
+
+#define CLRF_ALL 1u                    /* clear everything */
+static void clear_progress(struct ttyprogress *progress, unsigned f)
+       /* Clear the current progress display maintained by PROGRESS.
+        *
+        * If `CLRF_ALL' is set in F, then clear the entire display.
+        * Otherwise, clear the bottom few lines if there are now fewer
+        * progress items than there were last time we rendered the display,
+        * and leave the cursor at the start of the top line ready to
+        * overwrite it.
+        */
+{
+  struct tty *tty = progress->tty;
+  int ndel, nleave;
+
+  if (progress->last_lines) {
+
+    /* Decide how many lines to delete.  Set `ndel' to the number of lines
+     * that will be entirely erased, and `nleave' to the number that we'll
+     * leave.
+     */
+    if (f&CLRF_ALL)
+      { ndel = progress->last_lines; nleave = 0; }
+    else {
+      if (progress->nitems >= progress->last_lines) ndel = 0;
+      else ndel = progress->last_lines - progress->nitems;
+      nleave = progress->last_lines - ndel;
+    }
+
+    /* Now actually do the clearing.  Remember that the cursor is still on
+     * the last line.
+     */
+    if (!ndel)
+      tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1 - nleave, 0);
+    else {
+      tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1 - ndel, 0);
+      tty_erase(tty, TTEF_DSP | TTEF_END);
+      tty_move(tty, TTORG_CUR, -nleave, 0);
+    }
+  }
+
+  /* Remember that we're now at the top of the display. */
+  progress->last_lines = 0;
+}
+
+int ttyprogress_clear(struct ttyprogress *progress)
+{
+  if (!progress->tty) return (-1);
+  clear_progress(progress, CLRF_ALL);
+  return (0);
+}
+
+int ttyprogress_update(struct ttyprogress *progress)
+{
+  struct ttyprogress_render render;
+  struct ttyprogress_item *item;
+  struct tty *tty = progress->tty;
+  struct tty_attr save;
+  uint32 modes;
+  unsigned f = 0;
+#define f_any 1u
+
+  if (!tty) return (-1);
+
+  setup_render_state(progress, &render);
+  clear_progress(progress, 0);
+  modes = tty->st.modes; tty_setmodes(tty, TTMF_AUTOM, 0);
+  save = tty->st.attr;
+
+  for (item = progress->items; item; item = item->next) {
+    if (f&f_any) tty_move(tty, TTOF_YCUR | TTOF_XHOME, 1, 0);
+    render.leftsz = render.rightsz = 0;
+    render.leftwd = render.rightwd = 0;
+    item->render(item, &render); progress->last_lines++; f |= f_any;
+    if (progress->last_lines > tty->ht) break;
+  }
+  tty_setmodes(tty, MASK32, modes); tty_setattr(tty, &save);
+  fflush(tty->fpout);
+  return (0);
+
+#undef f_any
+}
+
+/*----- Rendering progress bars -------------------------------------------*/
+
+/* The basic problem here is to render text, formed of several pieces, to the
+ * terminal, placing some marker in the middle of it to indicate how much
+ * progress has been made.  This marker might be a colour change, switching
+ * off reverse-video mode, or a `|' character.
+ */
+
+struct bar {
+       /* State to track progress through the output of a progress bar, so
+        * that we insert the marker in the right place.
+        *
+        * This is a little state machine.  We remember the current column
+        * position, the current state, and the column at which we'll next
+        * change state.
+        */
+
+  const struct ttyprogress_render *render; /* render state */
+  unsigned pos, nextpos, state;                /* as described */
+};
+
+static void advance_bar_state(struct bar *bar)
+       /* If we've reached the column position for the next state change
+        * then arrange to do whatever it is we're meant to do, and update
+        * for the next change.
+        */
+{
+  const struct ttyprogress_render *render = bar->render;
+  struct tty *tty = render->tty;
+  size_t here = bar->nextpos;
+
+  while (bar->nextpos <= here) {
+    switch (bar->state) {
+      case LEFT:
+       if (!(tty->acaps&(TTACF_BG | TTACF_INVV))) putc('|', tty->fpout);
+       else tty_setattr(tty, &render->attr[TPHL_EBAR]);
+       bar->state = RIGHT; bar->nextpos = render->width;
+       break;
+      case RIGHT:
+       bar->state = STOP; bar->nextpos = UINT_MAX;
+       break;
+    }
+  }
+}
+
+/* Little utility to output a chunk of text. */
+static void put_str(FILE *fp, const char *p, size_t sz)
+  { while (sz--) putc(*p++, fp); }
+
+static void put_spc(struct tty *tty, unsigned n)
+{
+  if (!(~tty->ocaps&(TTCF_RELMV | TTCF_BGER | TTCF_ERCH)))
+    { tty_erch(tty, n); tty_move(tty, TTORG_CUR, 0, n); }
+  else
+    tty_repeat(tty, ' ', n);
+}
+
+static void put_barstr(struct bar *bar, const char *p, size_t sz)
+       /* Output the SZ-byte string P, driving the state machine BAR as we
+        * go.
+        */
+{
+  FILE *fp = bar->render->tty->fpout;
+  unsigned wd;
+  size_t n;
+
+  for (;;) {
+    /* Main loop.  Determine how much space there is to the next state
+     * change, cut off that amount of space from the string, and advance.
+     */
+
+    n = split_string(p, sz, &wd, bar->nextpos - bar->pos);
+    if (n == sz && wd < bar->nextpos - bar->pos) break;
+    put_str(fp, p, n); bar->pos += wd; advance_bar_state(bar);
+    p += n; sz -= n;
+  }
+
+  /* Write out the rest of the string, and update the position.  We know that
+   * this won't reach the next transition.
+   */
+  put_str(fp, p, sz); bar->pos += wd;
+}
+
+static void put_barspc(struct bar *bar, unsigned n)
+       /* Output N spaces, driving the state machine BAR as we go. */
+{
+  struct tty *tty = bar->render->tty;
+  unsigned step;
+
+  for (;;) {
+    step = bar->nextpos - bar->pos;
+    if (n < step) break;
+    put_spc(tty, step); bar->pos += step; n -= step;
+    advance_bar_state(bar);
+  }
+  put_spc(tty, n); bar->pos += n;
+}
+
+int ttyprogress_showbar(struct ttyprogress_render *render, double frac)
+{
+  struct tty *tty = render->tty;
+  const struct ttyprogress_buffer *line = render->line;
+  struct bar bar;
+
+  /* If there's no terminal, then there's nothing to do. */
+  if (!tty) return (-1);
+
+  /* Clamp the fraction.  Rounding errors in the caller's calculation might
+   * legitimately leave it slightly out of bounds.  The sense of the
+   * comparisons also catches QNaNs, which are silently squashed to zero so
+   * as to prevent anything untoward happening when we convert to integer
+   * arithmetic below.
+   */
+  frac = !(frac >= 0.0) ? 0.0 : !(frac <= 1.0) ? 1.0 : frac;
+
+  /* Set up the render state, with a transition where the bar should end. */
+  bar.render = render; bar.pos = 0; bar.nextpos = frac*render->width + 0.5;
+
+  /* Set the initial state for the render. */
+  if (tty->acaps&(TTACF_BG | TTACF_INVV)) {
+    /* We have highlighting.  If we have made only negligible progress then
+     * advance the state machine immediately, which will set the correct
+     * highlighting; otherwise, set the beginning-of-bar highlight.
+     */
+
+    bar.state = LEFT;
+    if (bar.nextpos) tty_setattr(tty, &render->attr[TPHL_BBAR]);
+    else advance_bar_state(&bar);
+  } else {
+    /* Nothing fancy.  We'll write `|' at the right place. */
+
+    bar.state = LEFT;
+  }
+
+  /* Write the left string, spaces to fill the gap, and the right string. */
+  put_barstr(&bar, line->p, render->leftsz);
+  put_barspc(&bar, render->width - render->leftwd - render->rightwd);
+  put_barstr(&bar, line->p + line->sz - render->rightsz, render->rightsz);
+
+  /* All done. */
+  return (0);
+}
+
+int ttyprogress_shownotice(struct ttyprogress_render *render,
+                          const struct tty_attr *attr)
+{
+  struct tty *tty = render->tty;
+  const struct ttyprogress_buffer *line = render->line;
+
+  /* If there's no terminal, then there's nothing to do. */
+  if (!tty->fpout) return (-1);
+
+  /* Set the attributes. */
+  tty_setattr(tty, attr);
+
+  /* Print the left and right strings. */
+  put_str(tty->fpout, line->p, render->leftsz);
+  put_spc(tty, render->width - render->leftwd - render->rightwd);
+  put_str(tty->fpout, line->p + line->sz - render->rightsz, render->rightsz);
+
+  /* All done. */
+  return (0);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/ui/ttyprogress.h b/ui/ttyprogress.h
new file mode 100644 (file)
index 0000000..1b207a5
--- /dev/null
@@ -0,0 +1,238 @@
+/* -*-c-*-
+ *
+ * Progress bars for terminal programs
+ *
+ * (c) 2025 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the mLib utilities library.
+ *
+ * mLib is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * mLib is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with mLib.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+#ifndef MLIB_TTYPROGRESS_H
+#define MLIB_TTYPROGRESS_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <stdio.h>
+#include <sys/time.h>
+
+#ifndef MLIB_ARENA_H
+#  include "arena.h"
+#endif
+
+#ifndef MLIB_DSTR_H
+#  include "dstr.h"
+#endif
+
+#ifndef MLIB_COMPILER_H
+#  include "compiler.h"
+#endif
+
+#ifndef MLIB_TTY_H
+#  include "tty.h"
+#endif
+
+#ifndef MLIB_TTYCOLOUR_H
+#  include "ttycolour.h"
+#endif
+
+/*------ Highlight definitions --------------------------------------------*/
+
+#define TTYPROGRESS_HIGHLIGHTS(_st, _)                                 \
+  _(_st, BBAR, "bb",   bbar_attrs)                                     \
+  _(_st, EBAR, "be",   ebar_attrs)                                     \
+  _(_st, NOTE, "nt",   note_attrs)                                     \
+  _(_st, WARN, "wr",   warn_attrs)                                     \
+  _(_st, ERR,  "er",   err_attrs)
+TTYCOLOUR_DEFENUM(TTYPROGRESS_HIGHLIGHTS, TPHL_);
+
+/*----- Data structures ---------------------------------------------------*/
+
+struct ttyprogress_render;
+
+struct ttyprogress_item {
+       /* An item in the progress display.
+        *
+        * The `render' function is passed a pointer to the
+        * `ttyprogress_item' structure.  Usually, it will need additional
+        * state: handle this by making the `ttyprogress_item' be the first
+        * member of a larger structure which holds the necessary
+        * information.
+        *
+        * The `render' function should limit its activities to actually
+        * writing a line of information to the terminal.  In particular, it
+        * shouldn't try to calculate anything time-dependent itself.
+        */
+
+  struct ttyprogress *parent;          /* controlling progress state */
+  struct ttyprogress_item *next, *prev;        /* forward and backward links */
+  void (*render)(struct ttyprogress_item */*item*/, /* render function */
+                struct ttyprogress_render */*render*/);
+};
+#define TTYPROGRESS_ITEM_INIT { 0, 0, 0, 0 }
+
+struct ttyprogress_buffer {
+  arena *a;                            /* owning arena */
+  dstr t;                              /* temporary string */
+  char *p;                             /* buffer pointer */
+  size_t sz;                           /* buffer size */
+};
+#define TTYPROGRESS_BUFFER_INIT { &arena_stdlib, DSTR_INIT, 0, 0 }
+
+struct ttyprogress {
+       /* The main state for progress reporting.  Here we keep track of the
+        * items which need to be displayed, and current state of the
+        * display.
+        */
+
+  struct tty *tty;                     /* terminal state */
+  struct ttyprogress_item *items, *end_item; /* list of progress items */
+  unsigned nitems;                     /* number of items */
+  unsigned last_lines;                 /* number written last time */
+  struct ttyprogress_buffer line;      /* line buffer */
+  struct timeval tv_update;            /* last update time */
+  struct tty_attr attr[TPHL__LIMIT];   /* highlight definitions */
+};
+#define TTYPROGRESS_INIT { 0, 0, 0, 0, 0, TTYPROGRESS_BUFFER_INIT, { 0, 0 } }
+
+struct ttyprogress_render {
+       /* Information passed to rendering functions.
+        *
+        * The `linebuf' accumulates the text to be shown by
+        * `ttyprogress_showbar' or similar, which consists of left and right
+        * portions aligned left and right on the terminal line, with a
+        * variable-size cap in between.  These strings are stored at the
+        * beginning and end of the `linebuf', so that (hopefully) new
+        * material can be added in the gap between them without us having to
+        * reallocate the buffer.
+        */
+
+  struct tty *tty;                     /* terminal state */
+  unsigned width;                      /* `effective' terminal width */
+  struct ttyprogress_buffer *line;     /* line buffer */
+  size_t leftsz, rightsz;              /* left and right cursors */
+  unsigned leftwd, rightwd;            /* left and right widths */
+  const struct tty_attr *attr;         /* highlight definitions */
+};
+
+/*----- Functions provided ------------------------------------------------*/
+
+extern int ttyprogress_init(struct ttyprogress */*progress*/,
+                           struct tty */*tty*/);
+       /* Initialize PROGRESS.
+        *
+        * It is safe to call this function on uninitialized data.
+        * Initialization involves opening a stream on the terminal and
+        * determining the terminal's capabilities.  Returns zero on success,
+        * or -1 on failure.  The structure is usable in either case (though
+        * if no terminal could be opened, then no progress output will be
+        * produced).
+        */
+
+extern void ttyprogress_free(struct ttyprogress */*progress*/);
+       /* Free any resources held by PROGRESS.
+        *
+        * It is safe to call this function on a structure that was
+        * initialized to `TTYPROGRESS_INIT', or by calling
+        * `ttyprogress_init', whether that function succeeded or not.  It's
+        * also harmless to call it repeatedly on the same structure.
+        */
+
+extern int ttyprogress_additem(struct ttyprogress */*progress*/,
+                              struct ttyprogress_item */*item*/);
+       /* If ITEM is already associated with a progress state, then do
+        * nothing and return -1.  Otherwise, add ITEM to the end of the list
+        * of active items maintained by PROGRESS, and return 0.  The
+        * progress display is not updated.
+        */
+
+extern int ttyprogress_removeitem(struct ttyprogress */*progress*/,
+                                 struct ttyprogress_item */*item*/);
+       /* If ITEM is not associated with a progress state, then do nothing
+        * and return -1.  Otherwise, remove ITEM from the list of active
+        * items maintained by PROGRESS, and return 0.  The progress display
+        * is not updated.
+        */
+
+extern int ttyprogress_clear(struct ttyprogress */*progress*/);
+       /* Clear any progress display currently shown on the terminal.  Call
+        * this before doing your own output to the terminal, and call
+        * `ttyprogress_update' afterwards.
+        */
+
+extern int ttyprogress_update(struct ttyprogress */*progress*/);
+       /* Update the progress display.  This will call the `render'
+        * functions for all active progress items to redraw them.
+        */
+
+/*----- Rendering primitives ----------------------------------------------*/
+
+extern int ttyprogress_vputleft(struct ttyprogress_render */*render*/,
+                               const char */*fmt*/, va_list */*ap*/);
+extern int ttyprogress_vputright(struct ttyprogress_render */*render*/,
+                                const char */*fmt*/, va_list */*ap*/);
+extern PRINTF_LIKE(2, 3)
+  int ttyprogress_putleft(struct ttyprogress_render */*render*/,
+                         const char */*fmt*/, ...);
+extern PRINTF_LIKE(2, 3)
+  int ttyprogress_putright(struct ttyprogress_render */*render*/,
+                          const char */*fmt*/, ...);
+       /* Format the `printf'-style string FMT with the supplied arguments
+        * and add it to the left or right side of the current line being
+        * built up in RENDER.  Later strings are added closer to the centre
+        * than earlier strings.  If there isn't enough space left to show
+        * the new string on a terminal line, or if there isn't enough memory
+        * for the necessary buffers, then do nothing and return -1.  If
+        * everything worked OK, then return 0.
+        */
+
+extern int ttyprogress_showbar(struct ttyprogress_render */*render*/,
+                              double /*frac*/);
+       /* Show a progress bar.  The text of the progress bar will be as
+        * established by the `ttyprogress_putleft' and
+        * `ttyprogress_putright' functions called on RENDER so far, and the
+        * bar will be written to the terminal associated with RENDER.  The
+        * length of the bar will be a FRAC fraction of the width of the
+        * terminal, so FRAC should be a real number between 0.0 and 1.0
+        * inclusive.
+        */
+
+extern int ttyprogress_shownotice(struct ttyprogress_render */*render*/,
+                                 const struct tty_attr */*attr*/);
+       /* Show a notice, i.e., a temporary message which doesn't actually
+        * have any progress associated with it.  The text of the notice will
+        * be as established by the `ttyprogress_putleft' and
+        * `ttyprogress_putright' functions called on RENDER so far, and the
+        * notice will be written to the terminal associated with RENDER.
+        * The notice's background and foreground colours will be BG and FG
+        * respectively.
+        */
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
index b48e6f68fb13335e31ca8cbebbe0adfc112a4125..4bf566348c8d3cc8ebeaf86d110ae71961af0538 100644 (file)
  * Returns:    The address of the containing @ty@ object.
  */
 
-#define CONTAINER(type, member, p)                                     \
-  (!sizeof((p) = &((type *)0)->mem) +                                  \
-   (type *)((unsigned char *)(p) - offsetof(type, mem)))
+#define CONTAINER(type, mem, p)                                                \
+       (!sizeof((p) = &((type *)0)->mem) +                             \
+        (type *)((unsigned char *)(p) - offsetof(type, mem)))
 
 /* --- @UNCONST@, @UNVOLATILE@, @UNQUALIFY@ --- *
  *