#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.
*
* 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: ---
*
* 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[] = {
/*
*/
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);
}
*/
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 */
return (0);
}
-/* --- @caps_claim@, @caps_release@ --- *
+/* --- @caps_release@ --- *
*
* Arguments: @struct tty *tty@ = control block pointer for current lock
* holder
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
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@ --- *
*
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; };
#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
#undef CLEARCAPS
}
-/* Macros for formatting capabilities. */
+/* --- @caps_padchars@ --- *
+ *
+ * Arguments: @struct tty *tty@ = extended control block pointer
+ * @unsigned delay@ = tenths of milliseconds required
+ * @unsigned f@ = flags
+ *
+ * Returns: The number of padding characters to send.
+ *
+ * Use: Determine the number of padding characters to send to achieve
+ * a delay of a given duration. If @CPF_FORCE@ is set, then
+ * ignore the @pb@ and @xon@ capabilities.
+ */
+
+#define CPF_FORCE 1u
+static size_t caps_padchars(struct tty *tty, unsigned delay, unsigned f)
+{
+ struct tty_caps *t = (struct tty_caps *)tty;
+
+ if (!(f&CPF_FORCE) && (t->tty.baud < t->cap.pb || t->cap.xon)) {
+ /* We're not forced to pad, and the baud rate is sufficiently low or we
+ * have flow control, then there's nothing to do.
+ */
+
+ return (0);
+ } else {
+ /* We're transmitting at R b/s, and we want to send N B of data so that
+ * this takes D/10000 s. We're likely sending either seven bits with
+ * parity or eight bits without, plus one stop bit, per character, so we
+ * must send 9 N b, which will take 9 N/R s = D/10000 s. Rearranging
+ * gives
+ *
+ * N = R D/90000 ,
+ *
+ * and we should round upwards.
+ */
+
+ return ((unsigned long)t->tty.baud*delay + 89999/90000);
+ }
+}
+
+#if defined(HAVE_TERMCAP) || defined(HAVE_TERMINFO)
+
+/* --- @caps_cost@ --- *
+ *
+ * Arguments: @struct tty *tty@ = control block pointer
+ * @unsigned npad@ = number of lines affected
+ * @const char *ctrl@ = formatted control string
+ *
+ * Returns: A linear `cost' for sending the capability.
+ *
+ * Use: Determines the cost for a `termcap' or `terminfo' capability
+ * by parsing a @tputs@-format string.
+ */
+
+static unsigned scan_delay(const char **p_inout, unsigned npad)
+{
+ const char *p = *p_inout;
+ unsigned t, f;
+
+#define f_padmul 1u
+
+ f = 0; t = 0;
+ while (ISDIGIT(*p)) t = 10*t + (*p++ - '0');
+ t *= 10;
+ if (*p == '.') {
+ p++;
+ if (ISDIGIT(*p)) {
+ t += *p++ - '0';
+ while (ISDIGIT(*p)) p++;
+ }
+ }
+ for (;;)
+ switch (*p) {
+ case '*': p++; f |= f_padmul; break;
+ case '/': p++; break;
+ default: goto done;
+ }
+done:
+ if (f&f_padmul) t *= npad;
+ *p_inout = p; return (npad);
+
+#undef f_padmul
+}
+
+static size_t caps_cost(struct tty *tty, unsigned npad, const char *ctrl)
+{
+ size_t n;
+ unsigned t;
+ const char *p;
+
+ p = ctrl;
+ if (ISDIGIT(*p)) {
+ /* The string starts with a number, so it's a `termcap'-style string with
+ * a leading millisecond count.
+ */
+
+ t = scan_delay(&p, npad);
+ n = strlen(p);
+ } else {
+ /* No initial number. Search for `terminfo'-style %|$<NNN.N[*|/]|%
+ * droppings.
+ */
+
+ n = 0; t = 0;
+ while (*p)
+ if (*p != '$' || p[1] != '<' || (!ISDIGIT(p[2] && p[2] != '>')))
+ { n++; p++; }
+ else {
+ p += 2; t += scan_delay(&p, npad);
+ if (*p == '>') p++;
+ }
+ }
+
+ /* All done. */
+ return (n + caps_padchars(tty, t, CPF_FORCE));
+}
+
+#endif
+
+/* Macros for formatting capabilities. The @...V@ macros evaluate the cap
+ * name, while the unmarked macros interpret it as a slot name.
+ */
#define PUT0V(npad, cap_) \
CHECK(ops->cap.put0(&t->tty, (npad), (cap_)))
#define PUT1IV(npad, cap_, i0) \
#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)
{
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)
#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)
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)
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,
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));
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));
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,
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)
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 {
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)
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)
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)
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)
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;
#undef PUT1I
#undef PUT2I
+/* The operation functions for terminal backends based on `terminfo'. */
#define TTY_CAPOPS \
caps_setattr, caps_setmodes, \
caps_move, caps_repeat, \
#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)
{
{
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)
{
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);
#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)
{
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)
{
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);
#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)
{
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)
{
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);
}
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);
}
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);
/*----- 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]
*
* 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,
*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;
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;
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;
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)
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));
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)
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));
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)
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)
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)
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)
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)
#undef CHECK
+/* The ANSI operations table. */
static const struct tty_ops ansi_ops = {
ansi_release,
ansi_setattr, ansi_setmodes,
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)
TTCF_DELCH | TTCF_DELLN | \
TTCF_INSCH | TTCF_INSLN)
+ /* Table of user-settable flags. */
static const struct flagmap {
const char *name;
uint32 acaps, ocaps;
{ 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 },
{ 0, 0 }
};
+ /* Table of enumerated settings. */
static const struct enummap {
const char *name;
uint32 mask;
{ 0, 0, 0 }
};
-
+ /* Table of recognized terminal types. */
static const struct termmap {
const char *pat;
unsigned acaps, ocaps, tf;
#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;
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))
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;
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);
/*----- 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 {
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) {
}
}
+/* --- @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;
/*----- 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;
}
}
+/* --- @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);
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);
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);
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);
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);
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);
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);
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);
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);
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);