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