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