patch 8.0.0693: no terminal emulator support

Problem:    No terminal emulator support.  Cannot properly run commands in the
            GUI.  Cannot run a job interactively with an ssh connection.
Solution:   Very early implementation of the :terminal command.  Includes
            libvterm converted to ANSI C.  Many parts still missing.
diff --git a/src/libvterm/bin/unterm.c b/src/libvterm/bin/unterm.c
new file mode 100644
index 0000000..acaf2d2
--- /dev/null
+++ b/src/libvterm/bin/unterm.c
@@ -0,0 +1,287 @@
+#include <stdio.h>
+#include <string.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "vterm.h"
+
+#define DEFINE_INLINES
+#include "../src/utf8.h" /* fill_utf8 */
+
+#define streq(a,b) (!strcmp(a,b))
+
+static VTerm *vt;
+static VTermScreen *vts;
+
+static int cols;
+static int rows;
+
+static enum {
+  FORMAT_PLAIN,
+  FORMAT_SGR
+} format = FORMAT_PLAIN;
+
+static int col2index(VTermColor target)
+{
+  int index;
+
+  for(index = 0; index < 256; index++) {
+    VTermColor col;
+    vterm_state_get_palette_color(NULL, index, &col);
+    if(col.red == target.red && col.green == target.green && col.blue == target.blue)
+      return index;
+  }
+  return -1;
+}
+
+static void dump_cell(const VTermScreenCell *cell, const VTermScreenCell *prevcell)
+{
+  switch(format) {
+    case FORMAT_PLAIN:
+      break;
+    case FORMAT_SGR:
+      {
+        /* If all 7 attributes change, that means 7 SGRs max */
+        /* Each colour could consume up to 3 */
+        int sgr[7 + 2*3]; int sgri = 0;
+
+        if(!prevcell->attrs.bold && cell->attrs.bold)
+          sgr[sgri++] = 1;
+        if(prevcell->attrs.bold && !cell->attrs.bold)
+          sgr[sgri++] = 22;
+
+        if(!prevcell->attrs.underline && cell->attrs.underline)
+          sgr[sgri++] = 4;
+        if(prevcell->attrs.underline && !cell->attrs.underline)
+          sgr[sgri++] = 24;
+
+        if(!prevcell->attrs.italic && cell->attrs.italic)
+          sgr[sgri++] = 3;
+        if(prevcell->attrs.italic && !cell->attrs.italic)
+          sgr[sgri++] = 23;
+
+        if(!prevcell->attrs.blink && cell->attrs.blink)
+          sgr[sgri++] = 5;
+        if(prevcell->attrs.blink && !cell->attrs.blink)
+          sgr[sgri++] = 25;
+
+        if(!prevcell->attrs.reverse && cell->attrs.reverse)
+          sgr[sgri++] = 7;
+        if(prevcell->attrs.reverse && !cell->attrs.reverse)
+          sgr[sgri++] = 27;
+
+        if(!prevcell->attrs.strike && cell->attrs.strike)
+          sgr[sgri++] = 9;
+        if(prevcell->attrs.strike && !cell->attrs.strike)
+          sgr[sgri++] = 29;
+
+        if(!prevcell->attrs.font && cell->attrs.font)
+          sgr[sgri++] = 10 + cell->attrs.font;
+        if(prevcell->attrs.font && !cell->attrs.font)
+          sgr[sgri++] = 10;
+
+        if(prevcell->fg.red   != cell->fg.red   ||
+            prevcell->fg.green != cell->fg.green ||
+            prevcell->fg.blue  != cell->fg.blue) {
+          int index = col2index(cell->fg);
+          if(index == -1)
+            sgr[sgri++] = 39;
+          else if(index < 8)
+            sgr[sgri++] = 30 + index;
+          else if(index < 16)
+            sgr[sgri++] = 90 + (index - 8);
+          else {
+            sgr[sgri++] = 38;
+            sgr[sgri++] = 5 | (1<<31);
+            sgr[sgri++] = index | (1<<31);
+          }
+        }
+
+        if(prevcell->bg.red   != cell->bg.red   ||
+            prevcell->bg.green != cell->bg.green ||
+            prevcell->bg.blue  != cell->bg.blue) {
+          int index = col2index(cell->bg);
+          if(index == -1)
+            sgr[sgri++] = 49;
+          else if(index < 8)
+            sgr[sgri++] = 40 + index;
+          else if(index < 16)
+            sgr[sgri++] = 100 + (index - 8);
+          else {
+            sgr[sgri++] = 48;
+            sgr[sgri++] = 5 | (1<<31);
+            sgr[sgri++] = index | (1<<31);
+          }
+        }
+
+        if(!sgri)
+          break;
+
+        printf("\x1b[");
+	{
+	  int i;
+	  for(i = 0; i < sgri; i++)
+	    printf(!i               ? "%d" :
+		sgr[i] & (1<<31) ? ":%d" :
+		";%d",
+		sgr[i] & ~(1<<31));
+	}
+        printf("m");
+      }
+      break;
+  }
+
+  {
+    int i;
+    for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell->chars[i]; i++) {
+      char bytes[6];
+      bytes[fill_utf8(cell->chars[i], bytes)] = 0;
+      printf("%s", bytes);
+    }
+  }
+}
+
+static void dump_eol(const VTermScreenCell *prevcell)
+{
+  switch(format) {
+    case FORMAT_PLAIN:
+      break;
+    case FORMAT_SGR:
+      if(prevcell->attrs.bold || prevcell->attrs.underline || prevcell->attrs.italic ||
+         prevcell->attrs.blink || prevcell->attrs.reverse || prevcell->attrs.strike ||
+         prevcell->attrs.font)
+        printf("\x1b[m");
+      break;
+  }
+
+  printf("\n");
+}
+
+void dump_row(int row)
+{
+  VTermPos pos;
+  VTermScreenCell prevcell;
+  pos.row = row;
+  pos.col = 0;
+  memset(&prevcell, 0, sizeof(prevcell));
+  vterm_state_get_default_colors(vterm_obtain_state(vt), &prevcell.fg, &prevcell.bg);
+
+  while(pos.col < cols) {
+    VTermScreenCell cell;
+    vterm_screen_get_cell(vts, pos, &cell);
+
+    dump_cell(&cell, &prevcell);
+
+    pos.col += cell.width;
+    prevcell = cell;
+  }
+
+  dump_eol(&prevcell);
+}
+
+static int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user)
+{
+  VTermScreenCell prevcell;
+  int col;
+
+  memset(&prevcell, 0, sizeof(prevcell));
+  vterm_state_get_default_colors(vterm_obtain_state(vt), &prevcell.fg, &prevcell.bg);
+
+  for(col = 0; col < cols; col++) {
+    dump_cell(cells + col, &prevcell);
+    prevcell = cells[col];
+  }
+
+  dump_eol(&prevcell);
+
+  return 1;
+}
+
+static int screen_resize(int new_rows, int new_cols, void *user)
+{
+  rows = new_rows;
+  cols = new_cols;
+  return 1;
+}
+
+static VTermScreenCallbacks cb_screen = {
+  NULL, /* damage */
+  NULL, /* moverect */
+  NULL, /* movecursor */
+  NULL, /* settermprop */
+  NULL, /* bell */
+  &screen_resize, /* resize */
+  &screen_sb_pushline, /* sb_pushline */
+  NULL, /* popline */
+};
+
+int main(int argc, char *argv[])
+{
+  int opt;
+  const char *file;
+  int fd;
+  int len;
+  char buffer[1024];
+  int row;
+
+  rows = 25;
+  cols = 80;
+
+  while((opt = getopt(argc, argv, "f:l:c:")) != -1) {
+    switch(opt) {
+      case 'f':
+        if(streq(optarg, "plain"))
+          format = FORMAT_PLAIN;
+        else if(streq(optarg, "sgr"))
+          format = FORMAT_SGR;
+        else {
+          fprintf(stderr, "Unrecognised format '%s'\n", optarg);
+          exit(1);
+        }
+        break;
+
+      case 'l':
+        rows = atoi(optarg);
+        if(!rows)
+          rows = 25;
+        break;
+
+      case 'c':
+        cols = atoi(optarg);
+        if(!cols)
+          cols = 80;
+        break;
+    }
+  }
+
+  file = argv[optind++];
+  fd = open(file, O_RDONLY);
+  if(fd == -1) {
+    fprintf(stderr, "Cannot open %s - %s\n", file, strerror(errno));
+    exit(1);
+  }
+
+  vt = vterm_new(rows, cols);
+  vterm_set_utf8(vt, true);
+
+  vts = vterm_obtain_screen(vt);
+  vterm_screen_set_callbacks(vts, &cb_screen, NULL);
+
+  vterm_screen_reset(vts, 1);
+
+  while((len = read(fd, buffer, sizeof(buffer))) > 0) {
+    vterm_input_write(vt, buffer, len);
+  }
+
+  for(row = 0; row < rows; row++) {
+    dump_row(row);
+  }
+
+  close(fd);
+
+  vterm_free(vt);
+  return 0;
+}
diff --git a/src/libvterm/bin/vterm-ctrl.c b/src/libvterm/bin/vterm-ctrl.c
new file mode 100644
index 0000000..14e35cd
--- /dev/null
+++ b/src/libvterm/bin/vterm-ctrl.c
@@ -0,0 +1,362 @@
+#define _XOPEN_SOURCE 500  /* strdup */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#define streq(a,b) (strcmp(a,b)==0)
+
+#include <termios.h>
+
+static char *getvalue(int *argip, int argc, char *argv[])
+{
+  if(*argip >= argc) {
+    fprintf(stderr, "Expected an option value\n");
+    exit(1);
+  }
+
+  return argv[(*argip)++];
+}
+
+static int getchoice(int *argip, int argc, char *argv[], const char *options[])
+{
+  const char *arg = getvalue(argip, argc, argv);
+
+  int value = -1;
+  while(options[++value])
+    if(streq(arg, options[value]))
+      return value;
+
+  fprintf(stderr, "Unrecognised option value %s\n", arg);
+  exit(1);
+}
+
+typedef enum {
+  OFF,
+  ON,
+  QUERY
+} BoolQuery;
+
+static BoolQuery getboolq(int *argip, int argc, char *argv[])
+{
+  const char *choices[] = {"off", "on", "query", NULL};
+  return getchoice(argip, argc, argv, choices);
+}
+
+static char *helptext[] = {
+  "reset",
+  "s8c1t [off|on]",
+  "keypad [app|num]",
+  "screen [off|on|query]",
+  "cursor [off|on|query]",
+  "curblink [off|on|query]",
+  "curshape [block|under|bar|query]",
+  "mouse [off|click|clickdrag|motion]",
+  "altscreen [off|on|query]",
+  "bracketpaste [off|on|query]",
+  "icontitle [STR]",
+  "icon [STR]",
+  "title [STR]",
+  NULL
+};
+
+static bool seticanon(bool icanon, bool echo)
+{
+  struct termios termios;
+
+  tcgetattr(0, &termios);
+
+  bool ret = (termios.c_lflag & ICANON);
+
+  if(icanon) termios.c_lflag |=  ICANON;
+  else       termios.c_lflag &= ~ICANON;
+
+  if(echo) termios.c_lflag |=  ECHO;
+  else     termios.c_lflag &= ~ECHO;
+
+  tcsetattr(0, TCSANOW, &termios);
+
+  return ret;
+}
+
+static void await_c1(int c1)
+{
+  int c;
+
+  /* await CSI - 8bit or 2byte 7bit form */
+  bool in_esc = false;
+  while((c = getchar())) {
+    if(c == c1)
+      break;
+    if(in_esc && c == (char)(c1 - 0x40))
+      break;
+    if(!in_esc && c == 0x1b)
+      in_esc = true;
+    else
+      in_esc = false;
+  }
+}
+
+static char *read_csi()
+{
+  unsigned char csi[32];
+  int i = 0;
+
+  await_c1(0x9B); /* CSI */
+
+  /* TODO: This really should be a more robust CSI parser
+   */
+  for(; i < sizeof(csi)-1; i++) {
+    int c = csi[i] = getchar();
+    if(c >= 0x40 && c <= 0x7e)
+      break;
+  }
+  csi[++i] = 0;
+
+  /* TODO: returns longer than 32? */
+
+  return strdup((char *)csi);
+}
+
+static char *read_dcs()
+{
+  unsigned char dcs[32];
+  bool in_esc = false;
+  int i;
+
+  await_c1(0x90);
+
+  for(i = 0; i < sizeof(dcs)-1; ) {
+    char c = getchar();
+    if(c == 0x9c) /* ST */
+      break;
+    if(in_esc && c == 0x5c)
+      break;
+    if(!in_esc && c == 0x1b)
+      in_esc = true;
+    else {
+      dcs[i++] = c;
+      in_esc = false;
+    }
+  }
+  dcs[++i] = 0;
+
+  return strdup((char *)dcs);
+}
+
+static void usage(int exitcode)
+{
+  char **p;
+
+  fprintf(stderr, "Control a libvterm-based terminal\n"
+      "\n"
+      "Options:\n");
+
+  for(p = helptext; *p; p++)
+    fprintf(stderr, "  %s\n", *p);
+
+  exit(exitcode);
+}
+
+static bool query_dec_mode(int mode)
+{
+  char *s = NULL;
+
+  printf("\x1b[?%d$p", mode);
+
+  do {
+    int reply_mode, reply_value;
+    char reply_cmd;
+
+    if(s)
+      free(s);
+    s = read_csi();
+
+    /* expect "?" mode ";" value "$y" */
+
+    /* If the sscanf format string ends in a literal, we can't tell from
+     * its return value if it matches. Hence we'll %c the cmd and check it
+     * explicitly
+     */
+    if(sscanf(s, "?%d;%d$%c", &reply_mode, &reply_value, &reply_cmd) < 3)
+      continue;
+    if(reply_cmd != 'y')
+      continue;
+
+    if(reply_mode != mode)
+      continue;
+
+    free(s);
+
+    if(reply_value == 1 || reply_value == 3)
+      return true;
+    if(reply_value == 2 || reply_value == 4)
+      return false;
+
+    printf("Unrecognised reply to DECRQM: %d\n", reply_value);
+    return false;
+  } while(1);
+}
+
+static void do_dec_mode(int mode, BoolQuery val, const char *name)
+{
+  switch(val) {
+    case OFF:
+    case ON:
+      printf("\x1b[?%d%c", mode, val == ON ? 'h' : 'l');
+      break;
+
+    case QUERY:
+      if(query_dec_mode(mode))
+        printf("%s on\n", name);
+      else
+        printf("%s off\n", name);
+      break;
+  }
+}
+
+static int query_rqss_numeric(char *cmd)
+{
+  char *s = NULL;
+
+  printf("\x1bP$q%s\x1b\\", cmd);
+
+  do {
+    int num;
+
+    if(s)
+      free(s);
+    s = read_dcs();
+
+    if(!s)
+      return -1;
+    if(strlen(s) < strlen(cmd))
+      return -1;
+    if(strcmp(s + strlen(s) - strlen(cmd), cmd) != 0) {
+      printf("No match\n");
+      continue;
+    }
+
+    if(s[0] != '1' || s[1] != '$' || s[2] != 'r')
+      return -1;
+
+    if(sscanf(s + 3, "%d", &num) != 1)
+      return -1;
+
+    return num;
+  } while(1);
+}
+
+bool wasicanon;
+
+void restoreicanon(void)
+{
+  seticanon(wasicanon, true);
+}
+
+int main(int argc, char *argv[])
+{
+  int argi = 1;
+
+  if(argc == 1)
+    usage(0);
+
+  wasicanon = seticanon(false, false);
+  atexit(restoreicanon);
+
+  while(argi < argc) {
+    const char *arg = argv[argi++];
+
+    if(streq(arg, "reset")) {
+      printf("\x1b" "c");
+    }
+    else if(streq(arg, "s8c1t")) {
+      const char *choices[] = {"off", "on", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b F"); break;
+      case 1:
+        printf("\x1b G"); break;
+      }
+    }
+    else if(streq(arg, "keypad")) {
+      const char *choices[] = {"app", "num", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b="); break;
+      case 1:
+        printf("\x1b>"); break;
+      }
+    }
+    else if(streq(arg, "screen")) {
+      do_dec_mode(5, getboolq(&argi, argc, argv), "screen");
+    }
+    else if(streq(arg, "cursor")) {
+      do_dec_mode(25, getboolq(&argi, argc, argv), "cursor");
+    }
+    else if(streq(arg, "curblink")) {
+      do_dec_mode(12, getboolq(&argi, argc, argv), "curblink");
+    }
+    else if(streq(arg, "curshape")) {
+      /* TODO: This ought to query the current value of DECSCUSR because it */
+      /*   may need blinking on or off */
+      const char *choices[] = {"block", "under", "bar", "query", NULL};
+      int shape = getchoice(&argi, argc, argv, choices);
+      switch(shape) {
+        case 3: /* query */
+          shape = query_rqss_numeric(" q");
+          switch(shape) {
+            case 1: case 2:
+              printf("curshape block\n");
+              break;
+            case 3: case 4:
+              printf("curshape under\n");
+              break;
+            case 5: case 6:
+              printf("curshape bar\n");
+              break;
+          }
+          break;
+
+        case 0:
+        case 1:
+        case 2:
+          printf("\x1b[%d q", 1 + (shape * 2));
+          break;
+      }
+    }
+    else if(streq(arg, "mouse")) {
+      const char *choices[] = {"off", "click", "clickdrag", "motion", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b[?1000l"); break;
+      case 1:
+        printf("\x1b[?1000h"); break;
+      case 2:
+        printf("\x1b[?1002h"); break;
+      case 3:
+        printf("\x1b[?1003h"); break;
+      }
+    }
+    else if(streq(arg, "altscreen")) {
+      do_dec_mode(1049, getboolq(&argi, argc, argv), "altscreen");
+    }
+    else if(streq(arg, "bracketpaste")) {
+      do_dec_mode(2004, getboolq(&argi, argc, argv), "bracketpaste");
+    }
+    else if(streq(arg, "icontitle")) {
+      printf("\x1b]0;%s\a", getvalue(&argi, argc, argv));
+    }
+    else if(streq(arg, "icon")) {
+      printf("\x1b]1;%s\a", getvalue(&argi, argc, argv));
+    }
+    else if(streq(arg, "title")) {
+      printf("\x1b]2;%s\a", getvalue(&argi, argc, argv));
+    }
+    else {
+      fprintf(stderr, "Unrecognised command %s\n", arg);
+      exit(1);
+    }
+  }
+  return 0;
+}
diff --git a/src/libvterm/bin/vterm-dump.c b/src/libvterm/bin/vterm-dump.c
new file mode 100644
index 0000000..84a957a
--- /dev/null
+++ b/src/libvterm/bin/vterm-dump.c
@@ -0,0 +1,231 @@
+/* Require getopt(3) */
+#define _XOPEN_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#define streq(a,b) (strcmp(a,b)==0)
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "vterm.h"
+
+static const char *special_begin = "{";
+static const char *special_end   = "}";
+
+static int parser_text(const char bytes[], size_t len, void *user)
+{
+  unsigned char *b = (unsigned char *)bytes;
+
+  int i;
+  for(i = 0; i < len; /* none */) {
+    if(b[i] < 0x20)        /* C0 */
+      break;
+    else if(b[i] < 0x80)   /* ASCII */
+      i++;
+    else if(b[i] < 0xa0)   /* C1 */
+      break;
+    else if(b[i] < 0xc0)   /* UTF-8 continuation */
+      break;
+    else if(b[i] < 0xe0) { /* UTF-8 2-byte */
+      /* 2-byte UTF-8 */
+      if(len < i+2) break;
+      i += 2;
+    }
+    else if(b[i] < 0xf0) { /* UTF-8 3-byte */
+      if(len < i+3) break;
+      i += 3;
+    }
+    else if(b[i] < 0xf8) { /* UTF-8 4-byte */
+      if(len < i+4) break;
+      i += 4;
+    }
+    else                   /* otherwise invalid */
+      break;
+  }
+
+  printf("%.*s", i, b);
+  return i;
+}
+
+/* 0     1      2      3       4     5      6      7      8      9      A      B      C      D      E      F    */
+static const char *name_c0[] = {
+  "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS",  "HT",  "LF",  "VT",  "FF",  "CR",  "LS0", "LS1",
+  "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM",  "SUB", "ESC", "FS",  "GS",  "RS",  "US",
+};
+static const char *name_c1[] = {
+  NULL,  NULL,  "BPH", "NBH", NULL,  "NEL", "SSA", "ESA", "HTS", "HTJ", "VTS", "PLD", "PLU", "RI",  "SS2", "SS3",
+  "DCS", "PU1", "PU2", "STS", "CCH", "MW",  "SPA", "EPA", "SOS", NULL,  "SCI", "CSI", "ST",  "OSC", "PM",  "APC",
+};
+
+static int parser_control(unsigned char control, void *user)
+{
+  if(control < 0x20)
+    printf("%s%s%s", special_begin, name_c0[control], special_end);
+  else if(control >= 0x80 && control < 0xa0 && name_c1[control - 0x80])
+    printf("%s%s%s", special_begin, name_c1[control - 0x80], special_end);
+  else
+    printf("%sCONTROL 0x%02x%s", special_begin, control, special_end);
+
+  if(control == 0x0a)
+    printf("\n");
+  return 1;
+}
+
+static int parser_escape(const char bytes[], size_t len, void *user)
+{
+  if(bytes[0] >= 0x20 && bytes[0] < 0x30) {
+    if(len < 2)
+      return -1;
+    len = 2;
+  }
+  else {
+    len = 1;
+  }
+
+  printf("%sESC %.*s%s", special_begin, (int)len, bytes, special_end);
+
+  return len;
+}
+
+/* 0     1      2      3       4     5      6      7      8      9      A      B      C      D      E      F    */
+static const char *name_csi_plain[] = {
+  "ICH", "CUU", "CUD", "CUF", "CUB", "CNL", "CPL", "CHA", "CUP", "CHT", "ED",  "EL",  "IL",  "DL",  "EF",  "EA",
+  "DCH", "SSE", "CPR", "SU",  "SD",  "NP",  "PP",  "CTC", "ECH", "CVT", "CBT", "SRS", "PTX", "SDS", "SIMD",NULL,
+  "HPA", "HPR", "REP", "DA",  "VPA", "VPR", "HVP", "TBC", "SM",  "MC",  "HPB", "VPB", "RM",  "SGR", "DSR", "DAQ",
+};
+
+/*0           4           8           B         */
+static const int newline_csi_plain[] = {
+  0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
+};
+
+static int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
+{
+  const char *name = NULL;
+  if(!leader && !intermed && command < 0x70)
+    name = name_csi_plain[command - 0x40];
+  else if(leader && streq(leader, "?") && !intermed) {
+    /* DEC */
+    switch(command) {
+      case 'h': name = "DECSM"; break;
+      case 'l': name = "DECRM"; break;
+    }
+    if(name)
+      leader = NULL;
+  }
+
+  if(!leader && !intermed && command < 0x70 && newline_csi_plain[command - 0x40])
+    printf("\n");
+
+  if(name)
+    printf("%s%s", special_begin, name);
+  else
+    printf("%sCSI", special_begin);
+
+  if(leader && leader[0])
+    printf(" %s", leader);
+
+  {
+    int i;
+    for(i = 0; i < argcount; i++) {
+      printf(i ? "," : " ");
+  }
+
+    if(args[i] == CSI_ARG_MISSING)
+      printf("*");
+    else {
+      while(CSI_ARG_HAS_MORE(args[i]))
+        printf("%ld+", CSI_ARG(args[i++]));
+      printf("%ld", CSI_ARG(args[i]));
+    }
+  }
+
+  if(intermed && intermed[0])
+    printf(" %s", intermed);
+
+  if(name)
+    printf("%s", special_end);
+  else
+    printf(" %c%s", command, special_end);
+
+  return 1;
+}
+
+static int parser_osc(const char *command, size_t cmdlen, void *user)
+{
+  printf("%sOSC %.*s%s", special_begin, (int)cmdlen, command, special_end);
+
+  return 1;
+}
+
+static int parser_dcs(const char *command, size_t cmdlen, void *user)
+{
+  printf("%sDCS %.*s%s", special_begin, (int)cmdlen, command, special_end);
+
+  return 1;
+}
+
+static VTermParserCallbacks parser_cbs = {
+  &parser_text, /* text */
+  &parser_control, /* control */
+  &parser_escape, /* escape */
+  &parser_csi, /* csi */
+  &parser_osc, /* osc */
+  &parser_dcs, /* dcs */
+  NULL /* resize */
+};
+
+int main(int argc, char *argv[])
+{
+  int use_colour = isatty(1);
+  const char *file;
+  int fd;
+  VTerm *vt;
+  int len;
+  char buffer[1024];
+
+  int opt;
+  while((opt = getopt(argc, argv, "c")) != -1) {
+    switch(opt) {
+      case 'c': use_colour = 1; break;
+    }
+  }
+
+  file = argv[optind++];
+
+  if(!file || streq(file, "-"))
+    fd = 0; /* stdin */
+  else {
+    fd = open(file, O_RDONLY);
+    if(fd == -1) {
+      fprintf(stderr, "Cannot open %s - %s\n", file, strerror(errno));
+      exit(1);
+    }
+  }
+
+  if(use_colour) {
+    special_begin = "\x1b[7m{";
+    special_end   = "}\x1b[m";
+  }
+
+  /* Size matters not for the parser */
+  vt = vterm_new(25, 80);
+  vterm_set_utf8(vt, 1);
+  vterm_parser_set_callbacks(vt, &parser_cbs, NULL);
+
+  while((len = read(fd, buffer, sizeof(buffer))) > 0) {
+    vterm_input_write(vt, buffer, len);
+  }
+
+  printf("\n");
+
+  close(fd);
+  vterm_free(vt);
+  return 0;
+}