#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include "automaton.h"
#include "internal.h"
#include "unixsig.h"
#include "render.h"
#include "in.h"

// Notcurses takes over stdin, and if it is not connected to a terminal, also
// tries to make a connection to the controlling terminal. If such a connection
// is made, it will read from that source (in addition to stdin). We dump one or
// both into distinct buffers. We then try to lex structured elements out of
// the buffer(s). We can extract cursor location reports, mouse events, and
// UTF-8 characters. Completely extracted ones are placed in their appropriate
// queues, and removed from the depository buffer. We aim to consume the
// entirety of the deposit before going back to read more data, but let anyone
// blocking on data wake up as soon as we've processed any input.
//
// The primary goal is to react to terminal messages (mostly cursor location
// reports) as quickly as possible, and definitely not with unbounded latency,
// without unbounded allocation, and also without losing data. We'd furthermore
// like to reliably differentiate escapes and regular input, even when that
// latter contains escapes. Unbounded input will hopefully only be present when
// redirected from a file.

static sig_atomic_t cont_seen;
static sig_atomic_t resize_seen;

// called for SIGWINCH and SIGCONT, and causes block_on_input to return
void sigwinch_handler(int signo){
  if(signo == SIGWINCH){
    resize_seen = signo;
    sigcont_seen_for_render = 1;
  }else if(signo == SIGCONT){
    cont_seen = signo;
    sigcont_seen_for_render = 1;
  }
}

typedef struct cursorloc {
  int y, x;             // 0-indexed cursor location
} cursorloc;

#ifndef __MINGW32__
typedef int ipipe;
#else
typedef HANDLE ipipe;
#endif

// local state for the input thread. don't put this large struct on the stack.
typedef struct inputctx {
  // these two are not ringbuffers; we always move any leftover materia to the
  // front of the queue (it ought be a handful of bytes at most).
  unsigned char tbuf[BUFSIZ]; // only used if we have distinct terminal fd
  unsigned char ibuf[BUFSIZ]; // might be intermingled bulk/control data

  int stdinfd;          // bulk in fd. always >= 0 (almost always 0). we do not
                        //  own this descriptor, and must not close() it.
  int termfd;           // terminal fd: -1 with no controlling terminal, or
                        //  if stdin is a terminal, or on MSFT Terminal.
#ifdef __MINGW32__
  HANDLE stdinhandle;   // handle to input terminal for MSFT Terminal
#endif

  int lmargin, tmargin; // margins in use at left and top
  int rmargin, bmargin; // margins in use at right and bottom

  automaton amata;
  int ibufvalid;      // we mustn't read() if ibufvalid == sizeof(ibuf)
  int tbufvalid;      // only used if we have distinct terminal connection

  uint8_t backspace;  // backspace is usually not an escape sequence, but
                      // instead ^H or ^? or something. only escape sequences
                      // go into our automaton, so we handle this one
                      // out-of-band. set to non-zero; match with ctrl.
  // ringbuffers for processed, structured input
  cursorloc* csrs;    // cursor reports are dumped here
  ncinput* inputs;    // processed input is dumped here
  int coutstanding;   // outstanding cursor location requests
  int csize, isize;   // total number of slots in csrs/inputs
  int cvalid, ivalid; // population count of csrs/inputs
  int cwrite, iwrite; // slot where we'll write the next csr/input;
                      //  we cannot write if valid == size
  int cread, iread;   // slot from which clients read the next csr/input;
                      //  they cannot read if valid == 0
  pthread_mutex_t ilock; // lock for ncinput ringbuffer, also initial state
  pthread_cond_t icond;  // condvar for ncinput ringbuffer
  pthread_mutex_t clock; // lock for csrs ringbuffer
  pthread_cond_t ccond;  // condvar for csrs ringbuffer
  tinfo* ti;          // link back to tinfo
  pthread_t tid;      // tid for input thread

  unsigned midescape; // we're in the middle of a potential escape. we need
                      //  to do a nonblocking read and try to complete it.
  unsigned stdineof;  // have we seen an EOF on stdin?

  unsigned linesigs;  // are line discipline signals active?
  unsigned drain;     // drain away bulk input?
  ncsharedstats *stats; // stats shared with notcurses context

  ipipe ipipes[2];
  ipipe readypipes[2]; // pipes[0]: poll()able fd indicating the presence of user input
  // initially, initdata is non-NULL and initdata_complete is NULL. once we
  // get DA1, initdata_complete is non-NULL (it is the same value as
  // initdata). once we complete reading the input payload that the DA1 arrived
  // in, initdata is set to NULL, and we broadcast availability. once it has
  // been taken, both become NULL.
  struct initial_responses* initdata;
  struct initial_responses* initdata_complete;
  int kittykbd;        // kitty keyboard protocol support level
  bool failed;         // error initializing input automaton, abort
} inputctx;

static inline void
inc_input_events(inputctx* ictx){
  pthread_mutex_lock(&ictx->stats->lock);
  ++ictx->stats->s.input_events;
  pthread_mutex_unlock(&ictx->stats->lock);
}

static inline void
inc_input_errors(inputctx* ictx){
  pthread_mutex_lock(&ictx->stats->lock);
  ++ictx->stats->s.input_errors;
  pthread_mutex_unlock(&ictx->stats->lock);
}

// load representations used by XTMODKEYS
static int
prep_xtmodkeys(inputctx* ictx){
  // XTMODKEYS enables unambiguous representations of certain inputs. We
  // enable XTMODKEYS where supported.
  static const struct {
    const char* esc;
    uint32_t key;
    unsigned modifiers;
  } keys[] = {
    { .esc = "\x1b\x8", .key = NCKEY_BACKSPACE,
      .modifiers = NCKEY_MOD_ALT, },
    { .esc = "\x1b[2P", .key = NCKEY_F01,
      .modifiers = NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[5P", .key = NCKEY_F01,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[6P", .key = NCKEY_F01,
      .modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[2Q", .key = NCKEY_F02,
      .modifiers = NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[5Q", .key = NCKEY_F02,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[6Q", .key = NCKEY_F02,
      .modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[2R", .key = NCKEY_F03,
      .modifiers = NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[5R", .key = NCKEY_F03,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[6R", .key = NCKEY_F03,
      .modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[2S", .key = NCKEY_F04,
      .modifiers = NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[5S", .key = NCKEY_F04,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[6S", .key = NCKEY_F04,
      .modifiers = NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT, },
    { .esc = NULL, .key = 0, },
  }, *k;
  for(k = keys ; k->esc ; ++k){
    if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key,
                                 k->modifiers)){
      return -1;
    }
    logdebug("added %u", k->key);
  }
  loginfo("added all xtmodkeys");
  return 0;
}

// load all known special keys from terminfo, and build the input sequence trie
static int
prep_special_keys(inputctx* ictx){
#ifndef __MINGW32__
  static const struct {
    const char* tinfo;
    uint32_t key;
    bool shift, ctrl, alt;
  } keys[] = {
    // backspace (kbs) is handled seperately at the end
    { .tinfo = "kbeg",  .key = NCKEY_BEGIN, },
    { .tinfo = "kcbt",  .key = '\t', .shift = true, }, // "back-tab"
    { .tinfo = "kcub1", .key = NCKEY_LEFT, },
    { .tinfo = "kcuf1", .key = NCKEY_RIGHT, },
    { .tinfo = "kcuu1", .key = NCKEY_UP, },
    { .tinfo = "kcud1", .key = NCKEY_DOWN, },
    { .tinfo = "kdch1", .key = NCKEY_DEL, },
    { .tinfo = "kbs",   .key = NCKEY_BACKSPACE, },
    { .tinfo = "kich1", .key = NCKEY_INS, },
    { .tinfo = "kend",  .key = NCKEY_END, },
    { .tinfo = "khome", .key = NCKEY_HOME, },
    { .tinfo = "knp",   .key = NCKEY_PGDOWN, },
    { .tinfo = "kpp",   .key = NCKEY_PGUP, },
    { .tinfo = "kf0",   .key = NCKEY_F01, },
    { .tinfo = "kf1",   .key = NCKEY_F01, },
    { .tinfo = "kf2",   .key = NCKEY_F02, },
    { .tinfo = "kf3",   .key = NCKEY_F03, },
    { .tinfo = "kf4",   .key = NCKEY_F04, },
    { .tinfo = "kf5",   .key = NCKEY_F05, },
    { .tinfo = "kf6",   .key = NCKEY_F06, },
    { .tinfo = "kf7",   .key = NCKEY_F07, },
    { .tinfo = "kf8",   .key = NCKEY_F08, },
    { .tinfo = "kf9",   .key = NCKEY_F09, },
    { .tinfo = "kf10",  .key = NCKEY_F10, },
    { .tinfo = "kf11",  .key = NCKEY_F11, },
    { .tinfo = "kf12",  .key = NCKEY_F12, },
    { .tinfo = "kf13",  .key = NCKEY_F13, },
    { .tinfo = "kf14",  .key = NCKEY_F14, },
    { .tinfo = "kf15",  .key = NCKEY_F15, },
    { .tinfo = "kf16",  .key = NCKEY_F16, },
    { .tinfo = "kf17",  .key = NCKEY_F17, },
    { .tinfo = "kf18",  .key = NCKEY_F18, },
    { .tinfo = "kf19",  .key = NCKEY_F19, },
    { .tinfo = "kf20",  .key = NCKEY_F20, },
    { .tinfo = "kf21",  .key = NCKEY_F21, },
    { .tinfo = "kf22",  .key = NCKEY_F22, },
    { .tinfo = "kf23",  .key = NCKEY_F23, },
    { .tinfo = "kf24",  .key = NCKEY_F24, },
    { .tinfo = "kf25",  .key = NCKEY_F25, },
    { .tinfo = "kf26",  .key = NCKEY_F26, },
    { .tinfo = "kf27",  .key = NCKEY_F27, },
    { .tinfo = "kf28",  .key = NCKEY_F28, },
    { .tinfo = "kf29",  .key = NCKEY_F29, },
    { .tinfo = "kf30",  .key = NCKEY_F30, },
    { .tinfo = "kf31",  .key = NCKEY_F31, },
    { .tinfo = "kf32",  .key = NCKEY_F32, },
    { .tinfo = "kf33",  .key = NCKEY_F33, },
    { .tinfo = "kf34",  .key = NCKEY_F34, },
    { .tinfo = "kf35",  .key = NCKEY_F35, },
    { .tinfo = "kf36",  .key = NCKEY_F36, },
    { .tinfo = "kf37",  .key = NCKEY_F37, },
    { .tinfo = "kf38",  .key = NCKEY_F38, },
    { .tinfo = "kf39",  .key = NCKEY_F39, },
    { .tinfo = "kf40",  .key = NCKEY_F40, },
    { .tinfo = "kf41",  .key = NCKEY_F41, },
    { .tinfo = "kf42",  .key = NCKEY_F42, },
    { .tinfo = "kf43",  .key = NCKEY_F43, },
    { .tinfo = "kf44",  .key = NCKEY_F44, },
    { .tinfo = "kf45",  .key = NCKEY_F45, },
    { .tinfo = "kf46",  .key = NCKEY_F46, },
    { .tinfo = "kf47",  .key = NCKEY_F47, },
    { .tinfo = "kf48",  .key = NCKEY_F48, },
    { .tinfo = "kf49",  .key = NCKEY_F49, },
    { .tinfo = "kf50",  .key = NCKEY_F50, },
    { .tinfo = "kf51",  .key = NCKEY_F51, },
    { .tinfo = "kf52",  .key = NCKEY_F52, },
    { .tinfo = "kf53",  .key = NCKEY_F53, },
    { .tinfo = "kf54",  .key = NCKEY_F54, },
    { .tinfo = "kf55",  .key = NCKEY_F55, },
    { .tinfo = "kf56",  .key = NCKEY_F56, },
    { .tinfo = "kf57",  .key = NCKEY_F57, },
    { .tinfo = "kf58",  .key = NCKEY_F58, },
    { .tinfo = "kf59",  .key = NCKEY_F59, },
    { .tinfo = "kent",  .key = NCKEY_ENTER, },
    { .tinfo = "kclr",  .key = NCKEY_CLS, },
    { .tinfo = "kc1",   .key = NCKEY_DLEFT, },
    { .tinfo = "kc3",   .key = NCKEY_DRIGHT, },
    { .tinfo = "ka1",   .key = NCKEY_ULEFT, },
    { .tinfo = "ka3",   .key = NCKEY_URIGHT, },
    { .tinfo = "kb2",   .key = NCKEY_CENTER, },
    { .tinfo = "kbeg",  .key = NCKEY_BEGIN, },
    { .tinfo = "kcan",  .key = NCKEY_CANCEL, },
    { .tinfo = "kclo",  .key = NCKEY_CLOSE, },
    { .tinfo = "kcmd",  .key = NCKEY_COMMAND, },
    { .tinfo = "kcpy",  .key = NCKEY_COPY, },
    { .tinfo = "kext",  .key = NCKEY_EXIT, },
    { .tinfo = "kprt",  .key = NCKEY_PRINT, },
    { .tinfo = "krfr",  .key = NCKEY_REFRESH, },
    { .tinfo = "kBEG",  .key = NCKEY_BEGIN, .shift = 1, },
    { .tinfo = "kBEG3", .key = NCKEY_BEGIN, .alt = 1, },
    { .tinfo = "kBEG4", .key = NCKEY_BEGIN, .alt = 1, .shift = 1, },
    { .tinfo = "kBEG5", .key = NCKEY_BEGIN, .ctrl = 1, },
    { .tinfo = "kBEG6", .key = NCKEY_BEGIN, .ctrl = 1, .shift = 1, },
    { .tinfo = "kBEG7", .key = NCKEY_BEGIN, .alt = 1, .ctrl = 1, },
    { .tinfo = "kDC",   .key = NCKEY_DEL, .shift = 1, },
    { .tinfo = "kDC3",  .key = NCKEY_DEL, .alt = 1, },
    { .tinfo = "kDC4",  .key = NCKEY_DEL, .alt = 1, .shift = 1, },
    { .tinfo = "kDC5",  .key = NCKEY_DEL, .ctrl = 1, },
    { .tinfo = "kDC6",  .key = NCKEY_DEL, .ctrl = 1, .shift = 1, },
    { .tinfo = "kDC7",  .key = NCKEY_DEL, .alt = 1, .ctrl = 1, },
    { .tinfo = "kDN",   .key = NCKEY_DOWN, .shift = 1, },
    { .tinfo = "kDN3",  .key = NCKEY_DOWN, .alt = 1, },
    { .tinfo = "kDN4",  .key = NCKEY_DOWN, .alt = 1, .shift = 1, },
    { .tinfo = "kDN5",  .key = NCKEY_DOWN, .ctrl = 1, },
    { .tinfo = "kDN6",  .key = NCKEY_DOWN, .ctrl = 1, .shift = 1, },
    { .tinfo = "kDN7",  .key = NCKEY_DOWN, .alt = 1, .ctrl = 1, },
    { .tinfo = "kEND",  .key = NCKEY_END, .shift = 1, },
    { .tinfo = "kEND3", .key = NCKEY_END, .alt = 1, },
    { .tinfo = "kEND4", .key = NCKEY_END, .alt = 1, .shift = 1, },
    { .tinfo = "kEND5", .key = NCKEY_END, .ctrl = 1, },
    { .tinfo = "kEND6", .key = NCKEY_END, .ctrl = 1, .shift = 1, },
    { .tinfo = "kEND7", .key = NCKEY_END, .alt = 1, .ctrl = 1, },
    { .tinfo = "kHOM",  .key = NCKEY_HOME, .shift = 1, },
    { .tinfo = "kHOM3", .key = NCKEY_HOME, .alt = 1, },
    { .tinfo = "kHOM4", .key = NCKEY_HOME, .alt = 1, .shift = 1, },
    { .tinfo = "kHOM5", .key = NCKEY_HOME, .ctrl = 1, },
    { .tinfo = "kHOM6", .key = NCKEY_HOME, .ctrl = 1, .shift = 1, },
    { .tinfo = "kHOM7", .key = NCKEY_HOME, .alt = 1, .ctrl = 1, },
    { .tinfo = "kIC",   .key = NCKEY_INS, .shift = 1, },
    { .tinfo = "kIC3",  .key = NCKEY_INS, .alt = 1, },
    { .tinfo = "kIC4",  .key = NCKEY_INS, .alt = 1, .shift = 1, },
    { .tinfo = "kIC5",  .key = NCKEY_INS, .ctrl = 1, },
    { .tinfo = "kIC6",  .key = NCKEY_INS, .ctrl = 1, .shift = 1, },
    { .tinfo = "kIC7",  .key = NCKEY_INS, .alt = 1, .ctrl = 1, },
    { .tinfo = "kLFT",  .key = NCKEY_LEFT, .shift = 1, },
    { .tinfo = "kLFT3", .key = NCKEY_LEFT, .alt = 1, },
    { .tinfo = "kLFT4", .key = NCKEY_LEFT, .alt = 1, .shift = 1, },
    { .tinfo = "kLFT5", .key = NCKEY_LEFT, .ctrl = 1, },
    { .tinfo = "kLFT6", .key = NCKEY_LEFT, .ctrl = 1, .shift = 1, },
    { .tinfo = "kLFT7", .key = NCKEY_LEFT, .alt = 1, .ctrl = 1, },
    { .tinfo = "kNXT",  .key = NCKEY_PGDOWN, .shift = 1, },
    { .tinfo = "kNXT3", .key = NCKEY_PGDOWN, .alt = 1, },
    { .tinfo = "kNXT4", .key = NCKEY_PGDOWN, .alt = 1, .shift = 1, },
    { .tinfo = "kNXT5", .key = NCKEY_PGDOWN, .ctrl = 1, },
    { .tinfo = "kNXT6", .key = NCKEY_PGDOWN, .ctrl = 1, .shift = 1, },
    { .tinfo = "kNXT7", .key = NCKEY_PGDOWN, .alt = 1, .ctrl = 1, },
    { .tinfo = "kPRV",  .key = NCKEY_PGUP, .shift = 1, },
    { .tinfo = "kPRV3", .key = NCKEY_PGUP, .alt = 1, },
    { .tinfo = "kPRV4", .key = NCKEY_PGUP, .alt = 1, .shift = 1, },
    { .tinfo = "kPRV5", .key = NCKEY_PGUP, .ctrl = 1, },
    { .tinfo = "kPRV6", .key = NCKEY_PGUP, .ctrl = 1, .shift = 1, },
    { .tinfo = "kPRV7", .key = NCKEY_PGUP, .alt = 1, .ctrl = 1, },
    { .tinfo = "kRIT",  .key = NCKEY_RIGHT, .shift = 1, },
    { .tinfo = "kRIT3", .key = NCKEY_RIGHT, .alt = 1, },
    { .tinfo = "kRIT4", .key = NCKEY_RIGHT, .alt = 1, .shift = 1, },
    { .tinfo = "kRIT5", .key = NCKEY_RIGHT, .ctrl = 1, },
    { .tinfo = "kRIT6", .key = NCKEY_RIGHT, .ctrl = 1, .shift = 1, },
    { .tinfo = "kRIT7", .key = NCKEY_RIGHT, .alt = 1, .ctrl = 1, },
    { .tinfo = "kUP",   .key = NCKEY_UP, .shift = 1, },
    { .tinfo = "kUP3",  .key = NCKEY_UP, .alt = 1, },
    { .tinfo = "kUP4",  .key = NCKEY_UP, .alt = 1, .shift = 1, },
    { .tinfo = "kUP5",  .key = NCKEY_UP, .ctrl = 1, },
    { .tinfo = "kUP6",  .key = NCKEY_UP, .ctrl = 1, .shift = 1, },
    { .tinfo = "kUP7",  .key = NCKEY_UP, .alt = 1, .ctrl = 1, },
    { .tinfo = NULL,    .key = 0, }
  }, *k;
  for(k = keys ; k->tinfo ; ++k){
    char* seq = tigetstr(k->tinfo);
    if(seq == NULL || seq == (char*)-1){
      loginfo("no terminfo declaration for %s", k->tinfo);
      continue;
    }
    if(seq[0] != NCKEY_ESC || strlen(seq) < 2){ // assume ESC prefix + content
      logwarn("invalid escape: %s (0x%x)", k->tinfo, k->key);
      continue;
    }
    unsigned modifiers = (k->shift ? NCKEY_MOD_SHIFT : 0)
                       | (k->alt ? NCKEY_MOD_ALT : 0)
                       | (k->ctrl ? NCKEY_MOD_CTRL : 0);
    if(inputctx_add_input_escape(&ictx->amata, seq, k->key, modifiers)){
      return -1;
    }
    logdebug("support for terminfo's %s: %s", k->tinfo, seq);
  }
  const char* bs = tigetstr("kbs");
  if(bs == NULL){
    logwarn("no backspace key was defined");
  }else{
    if(bs[0] == NCKEY_ESC){
      if(inputctx_add_input_escape(&ictx->amata, bs, NCKEY_BACKSPACE, 0)){
        return -1;
      }
    }else{
      ictx->backspace = bs[0];
    }
  }
#else
  (void)ictx;
#endif
  return 0;
}

// starting from the current amata match point, match any necessary prefix, then
// extract the (possibly empty) content, then match the follow. as we are only
// called from a callback context, and know we've been properly matched, there
// is no error-checking per se (we do require prefix/follow matches, but if
// missed, we just return NULL). indicate empty prefix with "", not NULL.
// updates ictx->amata.matchstart to be pointing past the follow. follow ought
// not be NUL.
static char*
amata_next_kleene(automaton* amata, const char* prefix, char follow){
  char c;
  while( (c = *prefix++) ){
    if(*amata->matchstart != c){
      logerror("matchstart didn't match prefix (%c vs %c)", c, *amata->matchstart);
      return NULL;
    }
    ++amata->matchstart;
  }
  // prefix has been matched. mark start of string and find follow.
  const unsigned char* start = amata->matchstart;
  while(*amata->matchstart != follow){
    ++amata->matchstart;
  }
  char* ret = malloc(amata->matchstart - start + 1);
  if(ret){
    memcpy(ret, start, amata->matchstart - start);
    ret[amata->matchstart - start] = '\0';
  }
  return ret;
}

// starting from the current amata match point, match any necessary prefix, then
// extract the numeric (possibly empty), then match the follow. as we are only
// called from a callback context, and know we've been properly matched, there
// is no error-checking per se (we do require prefix/follow matches, but if
// missed, we just return 0). indicate empty prefix with "", not NULL.
// updates ictx->amata.matchstart to be pointing past the follow. follow ought
// not be a digit nor NUL.
static unsigned
amata_next_numeric(automaton* amata, const char* prefix, char follow){
  char c;
  while( (c = *prefix++) ){
    if(*amata->matchstart != c){
      logerror("matchstart didn't match prefix (%c vs %c)", c, *amata->matchstart);
      return 0;
    }
    ++amata->matchstart;
  }
  // prefix has been matched
  unsigned ret = 0;
  while(isdigit(*amata->matchstart)){
    int addend = *amata->matchstart - '0';
    if((UINT_MAX - addend) / 10 < ret){
      logerror("overflow: %u * 10 + %u > %u", ret, addend, UINT_MAX);
    }
    ret *= 10;
    ret += addend;
    ++amata->matchstart;
  }
  char candidate = *amata->matchstart++;
  if(candidate != follow){
    logerror("didn't see follow (%c vs %c)", candidate, follow);
    return 0;
  }
  return ret;
}

// same deal as amata_next_numeric, but returns a heap-allocated string.
// strings always end with ST ("x1b\\"). this one *does* return NULL on
// either a match failure or an alloc failure.
static char*
amata_next_string(automaton* amata, const char* prefix){
  return amata_next_kleene(amata, prefix, '\x1b');
}

static inline void
send_synth_signal(int sig){
  if(sig){
#ifndef __MINGW32__
    raise(sig);
#endif
  }
}

static void
mark_pipe_ready(ipipe pipes[static 2]){
  char sig = 1;
#ifndef __MINGW32__
  if(write(pipes[1], &sig, sizeof(sig)) != 1){
    logwarn("error writing to pipe (%d) (%s)", pipes[1], strerror(errno));
#else
  DWORD wrote;
  if(!WriteFile(pipes[1], &sig, sizeof(sig), &wrote, NULL) || wrote != sizeof(sig)){
    logwarn("error writing to pipe");
#endif
  }else{
    loginfo("wrote to readiness pipe");
  }
}

// shove the assembled input |tni| into the input queue (if there's room, and
// we're not draining, and we haven't hit EOF). send any synthesized signal as
// the last thing we do. if Ctrl or Shift are among the modifiers, we replace
// any lowercase letter with its uppercase form, to maintain compatibility with
// other input methods.
//
// note that this w orks entirely off 'modifiers', not the obsolete
// shift/alt/ctrl booleans, which it neither sets nor tests!
static void
load_ncinput(inputctx* ictx, ncinput *tni){
  int synth = 0;
  if(tni->modifiers & (NCKEY_MOD_CTRL | NCKEY_MOD_SHIFT | NCKEY_MOD_CAPSLOCK)){
    // when ctrl/shift are used with an ASCII (0..127) lowercase letter, always
    // supply the capitalized form, to maintain compatibility among solutions.
    if(tni->id < 0x7f){
      if(islower(tni->id)){
        tni->id = toupper(tni->id);
      }
    }
  }
  // if the kitty keyboard protocol is in use, any input without an explicit
  // evtype can be safely considered a PRESS.
  if(ictx->kittykbd){
    if(tni->evtype == NCTYPE_UNKNOWN){
      tni->evtype = NCTYPE_PRESS;
    }
  }
  if(tni->modifiers == NCKEY_MOD_CTRL){ // exclude all other modifiers
    if(ictx->linesigs){
      if(tni->id == 'C'){
        synth = SIGINT;
      }else if(tni->id == 'Z'){
        synth = SIGSTOP;
      }else if(tni->id == '\\'){
        synth = SIGQUIT;
      }
    }
  }
  inc_input_events(ictx);
  if(ictx->drain || ictx->stdineof){
    send_synth_signal(synth);
    return;
  }
  pthread_mutex_lock(&ictx->ilock);
  if(ictx->ivalid == ictx->isize){
    pthread_mutex_unlock(&ictx->ilock);
    logwarn("dropping input 0x%08x", tni->id);
    inc_input_errors(ictx);
    send_synth_signal(synth);
    return;
  }
  ncinput* ni = ictx->inputs + ictx->iwrite;
  memcpy(ni, tni, sizeof(*tni));
  // perform final normalizations
  if(ni->id == 0x7f || ni->id == 0x8){
    ni->id = NCKEY_BACKSPACE;
  }else if(ni->id == '\n' || ni->id == '\r'){
    ni->id = NCKEY_ENTER;
  }else if(ni->id == ictx->backspace){
    ni->id = NCKEY_BACKSPACE;
  }else if(ni->id > 0 && ni->id <= 26 && ni->id != '\t'){
    ni->id = ni->id + 'A' - 1;
    ni->modifiers |= NCKEY_MOD_CTRL;
  }
  if(++ictx->iwrite == ictx->isize){
    ictx->iwrite = 0;
  }
  ++ictx->ivalid;
  // FIXME we don't always need to write here; write if ictx->ivalid was 0, and
  // also write *from the client context* if we empty the input buffer there..?
  mark_pipe_ready(ictx->readypipes);
  pthread_mutex_unlock(&ictx->ilock);
  pthread_cond_broadcast(&ictx->icond);
  send_synth_signal(synth);
}

static void
pixelmouse_click(inputctx* ictx, ncinput* ni, long y, long x){
  --x;
  --y;
  if(ictx->ti->cellpxy == 0 || ictx->ti->cellpxx == 0){
    logerror("pixelmouse event without pixel info (%ld/%ld)", y, x);
    inc_input_errors(ictx);
    return;
  }
  ni->ypx = y % ictx->ti->cellpxy;
  ni->xpx = x % ictx->ti->cellpxx;
  y /= ictx->ti->cellpxy;
  x /= ictx->ti->cellpxx;
  x -= ictx->lmargin;
  y -= ictx->tmargin;
  // convert from 1- to 0-indexing, and account for margins
  if(x < 0 || y < 0){ // click was in margins, drop it
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  if((unsigned)x >= ictx->ti->dimx - (ictx->rmargin + ictx->lmargin)){
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  if((unsigned)y >= ictx->ti->dimy - (ictx->bmargin + ictx->tmargin)){
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  ni->y = y;
  ni->x = x;
  load_ncinput(ictx, ni);
}

// ictx->numeric, ictx->p3, and ictx->p2 have the two parameters. we're using
// SGR (1006) mouse encoding, so use the final character to determine release
// ('M' for click, 'm' for release).
static void
mouse_click(inputctx* ictx, unsigned release, char follow){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[<", ';');
  long x = amata_next_numeric(&ictx->amata, "", ';');
  long y = amata_next_numeric(&ictx->amata, "", follow);
  ncinput tni = {
    .ctrl = mods & 0x10,
    .alt = mods & 0x08,
    .shift = mods & 0x04,
  };
  tni.modifiers = (tni.shift ? NCKEY_MOD_SHIFT : 0)
                  | (tni.ctrl ? NCKEY_MOD_CTRL : 0)
                  | (tni.alt ? NCKEY_MOD_ALT : 0);
  // SGR mouse reporting: lower two bits signify base button + {0, 1, 2} press
  // and no button pressed/release/{3}. bit 5 indicates motion. bits 6 and 7
  // select device groups: 64 is buttons 4--7, 128 is 8--11. a pure motion
  // report (no button) is 35 (32 + 3 (no button pressed)) with (oddly enough)
  // 'M' (i.e. release == true).
  if(release){
    tni.evtype = NCTYPE_RELEASE;
  }else{
    tni.evtype = NCTYPE_PRESS;
  }
  if(mods % 4 == 3){
    tni.id = NCKEY_MOTION;
    tni.evtype = NCTYPE_RELEASE;
  }else{
    if(mods < 64){
      tni.id = NCKEY_BUTTON1 + (mods % 4);
    }else if(mods >= 64 && mods < 128){
      tni.id = NCKEY_BUTTON4 + (mods % 4);
    }else if(mods >= 128 && mods < 192){
      tni.id = NCKEY_BUTTON8 + (mods % 4);
    }
  }
  if(ictx->ti->pixelmice){
    if(ictx->ti->cellpxx == 0){
      logerror("pixelmouse but no pixel info");
    }
    return pixelmouse_click(ictx, &tni, y, x);
  }
  x -= (1 + ictx->lmargin);
  y -= (1 + ictx->tmargin);
  // convert from 1- to 0-indexing, and account for margins
  if(x < 0 || y < 0){ // click was in margins, drop it
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  if((unsigned)x >= ictx->ti->dimx - (ictx->rmargin + ictx->lmargin)){
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  if((unsigned)y >= ictx->ti->dimy - (ictx->bmargin + ictx->tmargin)){
    logwarn("dropping click in margins %ld/%ld", y, x);
    return;
  }
  tni.x = x;
  tni.y = y;
  tni.ypx = -1;
  tni.xpx = -1;
  load_ncinput(ictx, &tni);
}

static int
mouse_press_cb(inputctx* ictx){
  mouse_click(ictx, 0, 'M');
  return 2;
}

static int
mouse_release_cb(inputctx* ictx){
  mouse_click(ictx, 1, 'm');
  return 2;
}

static int
cursor_location_cb(inputctx* ictx){
  unsigned y = amata_next_numeric(&ictx->amata, "\x1b[", ';') - 1;
  unsigned x = amata_next_numeric(&ictx->amata, "", 'R') - 1;
  // the first one doesn't go onto the queue; consume it here
  pthread_mutex_lock(&ictx->clock);
  --ictx->coutstanding;
  if(ictx->initdata){
    pthread_mutex_unlock(&ictx->clock);
    ictx->initdata->cursory = y;
    ictx->initdata->cursorx = x;
    return 2;
  }
  if(ictx->cvalid == ictx->csize){
    pthread_mutex_unlock(&ictx->clock);
    logwarn("dropping cursor location report %u/%u", y, x);
    inc_input_errors(ictx);
  }else{
    cursorloc* cloc = &ictx->csrs[ictx->cwrite];
    if(++ictx->cwrite == ictx->csize){
      ictx->cwrite = 0;
    }
    cloc->y = y;
    cloc->x = x;
    ++ictx->cvalid;
    pthread_mutex_unlock(&ictx->clock);
    pthread_cond_broadcast(&ictx->ccond);
    loginfo("cursor location: %u/%u", y, x);
  }
  return 2;
}

static int
geom_cb(inputctx* ictx){
  unsigned kind = amata_next_numeric(&ictx->amata, "\x1b[", ';');
  unsigned y = amata_next_numeric(&ictx->amata, "", ';');
  unsigned x = amata_next_numeric(&ictx->amata, "", 't');
  if(kind == 4){ // pixel geometry
    if(ictx->initdata){
      ictx->initdata->pixy = y;
      ictx->initdata->pixx = x;
    }
    loginfo("pixel geom report %d/%d", y, x);
  }else if(kind == 8){ // cell geometry
    if(ictx->initdata){
      ictx->initdata->dimy = y;
      ictx->initdata->dimx = x;
    }
    loginfo("cell geom report %d/%d", y, x);
  }else{
    logerror("invalid geom report type: %d", kind);
    return -1;
  }
  return 2;
}

static void
xtmodkey(inputctx* ictx, int val, int mods){
  assert(mods >= 0);
  assert(val > 0);
  logdebug("v/m %d %d", val, mods);
  ncinput tni = {
    .id = val,
    .evtype = NCTYPE_UNKNOWN,
  };
  if(mods == 2 || mods == 4 || mods == 6 || mods == 8 || mods == 10
      || mods == 12 || mods == 14 || mods == 16){
    tni.shift = 1;
    tni.modifiers |= NCKEY_MOD_SHIFT;
  }
  if(mods == 5 || mods == 6 || mods == 7 || mods == 8 ||
      (mods >= 13 && mods <= 16)){
    tni.ctrl = 1;
    tni.modifiers |= NCKEY_MOD_CTRL;
  }
  if(mods == 3 || mods == 4 || mods == 7 || mods == 8 || mods == 11
      || mods == 12 || mods == 15 || mods == 16){
    tni.alt = 1;
    tni.modifiers |= NCKEY_MOD_ALT;
  }
  if(mods >= 9 && mods <= 16){
    tni.modifiers |= NCKEY_MOD_META;
  }
  load_ncinput(ictx, &tni);
}

static uint32_t
kitty_functional(uint32_t val){
  if(val >= 57344 && val <= 63743){
    if(val >= 57376 && val <= 57398){
      val = NCKEY_F13 + val - 57376;
    }else if(val >= 57428 && val <= 57440){
      val = NCKEY_MEDIA_PLAY + val - 57428;
    }else if(val >= 57399 && val <= 57408){
      val = '0' + val - 57399;
    }else if(val >= 57441 && val <= 57454){ // up through NCKEY_L5SHIFT
      val = NCKEY_LSHIFT + val - 57441;
    }else switch(val){
      case 57358: val = NCKEY_CAPS_LOCK; break;
      case 57400: val = '1'; break;
      case 57359: val = NCKEY_SCROLL_LOCK; break;
      case 57360: val = NCKEY_NUM_LOCK; break;
      case 57361: val = NCKEY_PRINT_SCREEN; break;
      case 57362: val = NCKEY_PAUSE; break;
      case 57363: val = NCKEY_MENU; break;
      case 57409: val = '.'; break;
      case 57410: val = '/'; break;
      case 57411: val = '*'; break;
      case 57412: val = '-'; break;
      case 57413: val = '+'; break;
      case 57414: val = NCKEY_ENTER; break;
      case 57415: val = '='; break;
      case 57416: val = NCKEY_SEPARATOR; break;
      case 57417: val = NCKEY_LEFT; break;
      case 57418: val = NCKEY_RIGHT; break;
      case 57419: val = NCKEY_UP; break;
      case 57420: val = NCKEY_DOWN; break;
      case 57421: val = NCKEY_PGUP; break;
      case 57422: val = NCKEY_PGDOWN; break;
      case 57423: val = NCKEY_HOME; break;
      case 57424: val = NCKEY_END; break;
      case 57425: val = NCKEY_INS; break;
      case 57426: val = NCKEY_DEL; break;
      case 57427: val = NCKEY_BEGIN; break;
    }
  }else{
    switch(val){
      case 0xd: val = NCKEY_ENTER; break;
    }
  }
  return val;
}

static void
kitty_kbd(inputctx* ictx, int val, int mods, int evtype){
  assert(evtype >= 0);
  assert(mods >= 0);
  assert(val > 0);
  logdebug("v/m/e %d %d %d", val, mods, evtype);
  // "If the modifier field is not present in the escape code, its default value
  //  is 1 which means no modifiers."
  if(mods == 0){
    mods = 1;
  }
  ncinput tni = {
    .id = kitty_functional(val),
    .shift = mods && !!((mods - 1) & 0x1),
    .alt = mods && !!((mods - 1) & 0x2),
    .ctrl = mods && !!((mods - 1) & 0x4),
    .modifiers = mods - 1,
  };
  switch(evtype){
    case 0:
      __attribute__ ((fallthrough));
    case 1:
      tni.evtype = NCTYPE_PRESS;
      break;
    case 2:
      tni.evtype = NCTYPE_REPEAT;
      break;
    case 3:
      tni.evtype = NCTYPE_RELEASE;
      break;
    default:
      tni.evtype = NCTYPE_UNKNOWN;
      break;
  }
  load_ncinput(ictx, &tni);
}

static int
kitty_cb_simple(inputctx* ictx){
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", 'u');
  val = kitty_functional(val);
  kitty_kbd(ictx, val, 0, 0);
  return 2;
}

static int
kitty_cb(inputctx* ictx){
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
  unsigned mods = amata_next_numeric(&ictx->amata, "", 'u');
  kitty_kbd(ictx, val, mods, 0);
  return 2;
}

static uint32_t
legacy_functional(uint32_t id){
  switch(id){
    case 2: id = NCKEY_INS; break;
    case 3: id = NCKEY_DEL; break;
    case 5: id = NCKEY_PGUP; break;
    case 6: id = NCKEY_PGDOWN; break;
    case 7: id = NCKEY_HOME; break;
    case 8: id = NCKEY_END; break;
    case 11: id = NCKEY_F01; break;
    case 12: id = NCKEY_F02; break;
    case 13: id = NCKEY_F03; break;
    case 14: id = NCKEY_F04; break;
    case 15: id = NCKEY_F05; break;
    case 17: id = NCKEY_F06; break;
    case 18: id = NCKEY_F07; break;
    case 19: id = NCKEY_F08; break;
    case 20: id = NCKEY_F09; break;
    case 21: id = NCKEY_F10; break;
    case 23: id = NCKEY_F11; break;
    case 24: id = NCKEY_F12; break;
  }
  return id;
}

static int
simple_cb_begin(inputctx* ictx){
  kitty_kbd(ictx, NCKEY_BEGIN, 0, 0);
  return 2;
}

static int
kitty_cb_functional(inputctx* ictx){
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
  unsigned mods = amata_next_numeric(&ictx->amata, "", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", '~');
  uint32_t kval = kitty_functional(val);
  if(kval == val){
    kval = legacy_functional(val);
  }
  kitty_kbd(ictx, kval, mods, ev);
  return 2;
}

static int
wezterm_cb(inputctx* ictx){
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
  unsigned mods = amata_next_numeric(&ictx->amata, "", '~');
  uint32_t kval = legacy_functional(val);
  kitty_kbd(ictx, kval, mods, 0);
  return 2;
}

static int
legacy_cb_f1(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'P');
  kitty_kbd(ictx, NCKEY_F01, mods, 0);
  return 2;
}

static int
legacy_cb_f2(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'Q');
  kitty_kbd(ictx, NCKEY_F02, mods, 0);
  return 2;
}

static int
legacy_cb_f4(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'S');
  kitty_kbd(ictx, NCKEY_F04, mods, 0);
  return 2;
}

static int
kitty_cb_f1(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'P');
  kitty_kbd(ictx, NCKEY_F01, mods, ev);
  return 2;
}

static int
kitty_cb_f2(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'Q');
  kitty_kbd(ictx, NCKEY_F02, mods, ev);
  return 2;
}

static int
kitty_cb_f3(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'R');
  kitty_kbd(ictx, NCKEY_F03, mods, ev);
  return 2;
}

static int
kitty_cb_f4(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'S');
  kitty_kbd(ictx, NCKEY_F04, mods, ev);
  return 2;
}

static int
legacy_cb_right(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'C');
  kitty_kbd(ictx, NCKEY_RIGHT, mods, 0);
  return 2;
}

static int
legacy_cb_left(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'D');
  kitty_kbd(ictx, NCKEY_LEFT, mods, 0);
  return 2;
}

static int
legacy_cb_down(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'B');
  kitty_kbd(ictx, NCKEY_DOWN, mods, 0);
  return 2;
}

static int
legacy_cb_up(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'A');
  kitty_kbd(ictx, NCKEY_UP, mods, 0);
  return 2;
}

static int
kitty_cb_right(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'C');
  kitty_kbd(ictx, NCKEY_RIGHT, mods, ev);
  return 2;
}

static int
kitty_cb_left(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'D');
  kitty_kbd(ictx, NCKEY_LEFT, mods, ev);
  return 2;
}

static int
kitty_cb_down(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'B');
  kitty_kbd(ictx, NCKEY_DOWN, mods, ev);
  return 2;
}

static int
kitty_cb_up(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'A');
  kitty_kbd(ictx, NCKEY_UP, mods, ev);
  return 2;
}

static int
legacy_cb_begin(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'E');
  kitty_kbd(ictx, NCKEY_BEGIN, mods, 0);
  return 2;
}

static int
legacy_cb_end(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'F');
  kitty_kbd(ictx, NCKEY_END, mods, 0);
  return 2;
}

static int
legacy_cb_home(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", 'H');
  kitty_kbd(ictx, NCKEY_HOME, mods, 0);
  return 2;
}

static int
kitty_cb_begin(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'E');
  kitty_kbd(ictx, NCKEY_BEGIN, mods, ev);
  return 2;
}

static int
kitty_cb_end(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'F');
  kitty_kbd(ictx, NCKEY_END, mods, ev);
  return 2;
}

static int
kitty_cb_home(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[1;", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'H');
  kitty_kbd(ictx, NCKEY_HOME, mods, ev);
  return 2;
}

static int
kitty_cb_complex(inputctx* ictx){
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[", ';');
  unsigned mods = amata_next_numeric(&ictx->amata, "", ':');
  unsigned ev = amata_next_numeric(&ictx->amata, "", 'u');
  val = kitty_functional(val);
  kitty_kbd(ictx, val, mods, ev);
  return 2;
}

static int
kitty_keyboard_cb(inputctx* ictx){
  unsigned level = amata_next_numeric(&ictx->amata, "\x1b[?", 'u');
  if(ictx->initdata){
    ictx->initdata->kbdlevel = level;
  }
  loginfo("kitty keyboard level %u (was %u)", level, ictx->kittykbd);
  ictx->kittykbd = level;
  return 2;
}

static int
xtmodkey_cb(inputctx* ictx){
  unsigned mods = amata_next_numeric(&ictx->amata, "\x1b[27;", ';');
  unsigned val = amata_next_numeric(&ictx->amata, "", '~');
  xtmodkey(ictx, val, mods);
  return 2;
}

// the only xtsmgraphics reply with a single Pv arg is color registers
static int
xtsmgraphics_cregs_cb(inputctx* ictx){
  unsigned pv = amata_next_numeric(&ictx->amata, "\x1b[?1;0;", 'S');
  if(ictx->initdata){
    ictx->initdata->color_registers = pv;
  }
  loginfo("sixel color registers: %d", pv);
  return 2;
}

// the only xtsmgraphics reply with a dual Pv arg we want is sixel geometry
static int
xtsmgraphics_sixel_cb(inputctx* ictx){
  unsigned width = amata_next_numeric(&ictx->amata, "\x1b[?2;0;", ';');
  unsigned height = amata_next_numeric(&ictx->amata, "", 'S');
  if(ictx->initdata){
    ictx->initdata->sixelx = width;
    ictx->initdata->sixely = height;
  }
  loginfo("max sixel geometry: %dx%d", height, width);
  return 2;
}

static void
handoff_initial_responses_late(inputctx* ictx){
  bool sig = false;
  pthread_mutex_lock(&ictx->ilock);
  if(ictx->initdata_complete && ictx->initdata){
     ictx->initdata = NULL;
     sig = true;
  }
  pthread_mutex_unlock(&ictx->ilock);
  if(sig){
    pthread_cond_broadcast(&ictx->icond);
    loginfo("handing off initial responses");
  }
}


// mark the initdata as complete, but don't yet broadcast it off.
static void
handoff_initial_responses_early(inputctx* ictx){
  pthread_mutex_lock(&ictx->ilock);
  // set initdata_complete, but don't clear initdata
  ictx->initdata_complete = ictx->initdata;
  pthread_mutex_unlock(&ictx->ilock);
}

// if XTSMGRAPHICS responses were provided, but DA1 didn't advertise sixel
// support, we need scrub those responses, lest we try to use Sixel.
static inline void
scrub_sixel_responses(struct initial_responses* idata){
  if(idata->color_registers || idata->sixelx || idata->sixely){
    logwarn("answered XTSMGRAPHICS, but no sixel in DA1");
    idata->color_registers = 0;
    idata->sixelx = 0;
    idata->sixely = 0;
  }
}

// annoyingly, alacritty (well, branches of alacritty) supports Sixel, but
// does not indicate this in their Primary Device Attributes response (there
// is no room for attributes in a VT102-style DA1, which alacritty uses).
// so, iff we've determined we're alacritty, don't scrub out Sixel details.
static int
da1_vt102_cb(inputctx* ictx){
  loginfo("read primary device attributes");
  if(ictx->initdata){
    if(ictx->initdata->qterm != TERMINAL_ALACRITTY){
      scrub_sixel_responses(ictx->initdata);
    }
    handoff_initial_responses_early(ictx);
  }
  return 1;
}

static int
da1_cb(inputctx* ictx){
  loginfo("read primary device attributes");
  if(ictx->initdata){
    scrub_sixel_responses(ictx->initdata);
    handoff_initial_responses_early(ictx);
  }
  return 1;
}

static int
da1_attrs_cb(inputctx* ictx){
  loginfo("read primary device attributes");
  unsigned val = amata_next_numeric(&ictx->amata, "\x1b[?", ';');
  char* attrlist = amata_next_kleene(&ictx->amata, "", 'c');
  logdebug("DA1: %u [%s]", val, attrlist);
  if(ictx->initdata){
    int foundsixel = 0;
    unsigned curattr = 0;
    for(const char* a = attrlist ; *a ; ++a){
      if(isdigit(*a)){
        curattr *= 10;
        curattr += *a - '0';
      }else if(*a == ';'){
        if(curattr == 4){
          foundsixel = 1;
          if(ictx->initdata->color_registers <= 0){
            ictx->initdata->color_registers = 256;
          }
        }else if(curattr == 28){
          ictx->initdata->rectangular_edits = true;
        }
        curattr = 0;
      }
    }
    if(curattr == 4){
      foundsixel = 1;
      if(ictx->initdata->color_registers <= 0){
        ictx->initdata->color_registers = 256;
      }
    }else if(curattr == 28){
      ictx->initdata->rectangular_edits = true;
    }
    if(!foundsixel){
      scrub_sixel_responses(ictx->initdata);
    }
    handoff_initial_responses_early(ictx);
  }
  free(attrlist);
  return 1;
}

// GNU screen primarily identifies itself via an "83" as the first parameter
// of its DA2 reply. the version is the second parameter.
static int
da2_screen_cb(inputctx* ictx){
  if(ictx->initdata == NULL){
    return 2;
  }
  if(ictx->initdata->qterm != TERMINAL_UNKNOWN){
    logwarn("already identified term (%d)", ictx->initdata->qterm);
    return 2;
  }
  unsigned ver = amata_next_numeric(&ictx->amata, "\x1b[>83;", ';');
  if(ver < 10000){
    logwarn("version %u doesn't look like GNU screen", ver);
    return 2;
  }
  char verstr[9]; // three two-digit components plus two delims
  int s = snprintf(verstr, sizeof(verstr), "%u.%02u.%02u",
                   ver / 10000, ver / 100 % 100, ver % 100);
  if(s < 0 || (unsigned)s >= sizeof(verstr)){
    logwarn("bad screen version %u", ver);
    return 2;
  }
  ictx->initdata->version = strdup(verstr);
  ictx->initdata->qterm = TERMINAL_GNUSCREEN;
  return 2;
}

// we use secondary device attributes to recognize the alacritty crate
// version, and to ascertain the version of old, pre-XTVERSION XTerm.
static int
da2_cb(inputctx* ictx){
  loginfo("read secondary device attributes");
  if(ictx->initdata == NULL){
    return 2;
  }
  amata_next_numeric(&ictx->amata, "\x1b[>", ';');
  unsigned pv = amata_next_numeric(&ictx->amata, "", ';');
  int maj, min, patch;
  if(pv == 0){
    return 2;
  }
  // modern XTerm replies to XTVERSION, but older versions require extracting
  // the version from secondary DA
  if(ictx->initdata->qterm == TERMINAL_XTERM){
    if(ictx->initdata->version == NULL){
      char ver[8];
      int s = snprintf(ver, sizeof(ver), "%u", pv);
      if(s < 0 || (unsigned)s >= sizeof(ver)){
        logerror("bad version: %u", pv);
      }else{
        ictx->initdata->version = strdup(ver);
      }
      return 2;
    }
  }
  // SDA yields up Alacritty's crate version, but it doesn't unambiguously
  // identify Alacritty. If we've got any other version information, don't
  // use this. use the second parameter (Pv).
  if(ictx->initdata->qterm != TERMINAL_UNKNOWN || ictx->initdata->version){
    loginfo("termtype was %d %s, not alacritty", ictx->initdata->qterm,
            ictx->initdata->version);
    return 2;
  }
  // if a termname was manually supplied in setup, it was written to the env
  const char* termname = getenv("TERM");
  if(termname == NULL || strstr(termname, "alacritty") == NULL){
    loginfo("termname was [%s], probably not alacritty",
            termname ? termname : "unset");
    return 2;
  }
  maj = pv / 10000;
  min = (pv % 10000) / 100;
  patch = pv % 100;
  if(maj >= 100 || min >= 100 || patch >= 100){
    return 2;
  }
  // 3x components (two digits max each), 2x '.', NUL would suggest 9 bytes,
  // but older gcc __builtin___sprintf_chk insists on 13. fuck it. FIXME.
  char* buf = malloc(13);
  if(buf){
    sprintf(buf, "%d.%d.%d", maj, min, patch);
    loginfo("might be alacritty %s", buf);
    ictx->initdata->version = buf;
    ictx->initdata->qterm = TERMINAL_ALACRITTY;
  }
  return 2;
}

// weird form of Ternary Device Attributes used only by WezTerm
static int
wezterm_tda_cb(inputctx* ictx){
  if(ictx->initdata){
    loginfo("read ternary device attributes");
  }
  return 2;
}

static int
kittygraph_cb(inputctx* ictx){
  loginfo("kitty graphics message");
  if(ictx->initdata){
    ictx->initdata->kitty_graphics = 1;
  }
  return 2;
}

static int
decrpm_pixelmice(inputctx* ictx){
  unsigned ps = amata_next_numeric(&ictx->amata, "\x1b[?1016;", '$');
  loginfo("received decrpm 1016 %u", ps);
  if(ps == 2){
    if(ictx->initdata){
      ictx->initdata->pixelmice = 1;
    }
  }
  return 2;
}

static int
decrpm_asu_cb(inputctx* ictx){
  unsigned ps = amata_next_numeric(&ictx->amata, "\x1b[?2026;", '$');
  loginfo("received decrpm 2026 %u", ps);
  if(ps == 2){
    if(ictx->initdata){
      ictx->initdata->appsync_supported = 1;
    }
  }
  return 2;
}

static int
get_default_color(const char* str, uint32_t* color){
  int r, g, b;
  if(sscanf(str, "%02x/%02x/%02x", &r, &g, &b) == 3){
    // great! =]
  }else if(sscanf(str, "%04x/%04x/%04x", &r, &g, &b) == 3){
    r /= 256;
    g /= 256;
    b /= 256;
  }else{
    logerror("couldn't extract rgb from %s", str);
    return -1;
  }
  if(r < 0 || g < 0 || b < 0){
    logerror("invalid colors %d %d %d", r, g, b);
    return -1;
  }
  *color = (r << 16u) | (g << 8u) | b;
  return 0;
}

static int
bgdef_cb(inputctx* ictx){
  if(ictx->initdata){
    char* str = amata_next_string(&ictx->amata, "\x1b]11;rgb:");
    if(str == NULL){
      logerror("empty bg string");
    }else{
      if(get_default_color(str, &ictx->initdata->bg) == 0){
        ictx->initdata->got_bg = true;
        loginfo("default background 0x%06x", ictx->initdata->bg);
      }
      free(str);
    }
  }
  return 2;
}

static int
fgdef_cb(inputctx* ictx){
  if(ictx->initdata){
    char* str = amata_next_string(&ictx->amata, "\x1b]10;rgb:");
    if(str == NULL){
      logerror("empty fg string");
    }else{
      if(get_default_color(str, &ictx->initdata->fg) == 0){
        ictx->initdata->got_fg = true;
        loginfo("default foreground 0x%06x", ictx->initdata->fg);
      }
      free(str);
    }
  }
  return 2;
}

static int
palette_cb(inputctx* ictx){
  if(ictx->initdata){
    unsigned idx = amata_next_numeric(&ictx->amata, "\x1b]4;", ';');
    char* str = amata_next_string(&ictx->amata, "rgb:");
    if(idx > sizeof(ictx->initdata->palette.chans) / sizeof(*ictx->initdata->palette.chans)){
      logerror("invalid index %u", idx);
    }else if(str == NULL){
      logerror("empty palette string");
    }else{
      if(get_default_color(str, &ictx->initdata->palette.chans[idx]) == 0){
        if((int)idx > ictx->initdata->maxpaletteread){
          ictx->initdata->maxpaletteread = idx;
        }
        logverbose("index %u 0x%06x", idx, ictx->initdata->palette.chans[idx]);
      }
      free(str);
    }
  }
  return 2;
}

static int
extract_xtversion(inputctx* ictx, const char* str, char suffix){
  size_t slen = strlen(str);
  if(slen == 0){
    logwarn("empty version in xtversion");
    return -1;
  }
  if(suffix){
    if(str[slen - 1] != suffix){
      return -1;
    }
    --slen;
  }
  if(slen == 0){
    logwarn("empty version in xtversion");
    return -1;
  }
  ictx->initdata->version = strndup(str, slen);
  return 0;
}

static int
xtversion_cb(inputctx* ictx){
  if(ictx->initdata == NULL){
    return 2;
  }
  char* xtversion = amata_next_string(&ictx->amata, "\x1bP>|");
  if(xtversion == NULL){
    logwarn("empty xtversion");
    return 2; // don't replay as input
  }
  static const struct {
    const char* prefix;
    char suffix;
    queried_terminals_e term;
  } xtvers[] = {
    { .prefix = "XTerm(", .suffix = ')', .term = TERMINAL_XTERM, },
    { .prefix = "WezTerm ", .suffix = 0, .term = TERMINAL_WEZTERM, },
    { .prefix = "contour ", .suffix = 0, .term = TERMINAL_CONTOUR, },
    { .prefix = "kitty(", .suffix = ')', .term = TERMINAL_KITTY, },
    { .prefix = "foot(", .suffix = ')', .term = TERMINAL_FOOT, },
    { .prefix = "mlterm(", .suffix = ')', .term = TERMINAL_MLTERM, },
    { .prefix = "tmux ", .suffix = 0, .term = TERMINAL_TMUX, },
    { .prefix = "iTerm2 ", .suffix = 0, .term = TERMINAL_ITERM, },
    { .prefix = "mintty ", .suffix = 0, .term = TERMINAL_MINTTY, },
    { .prefix = "terminology ", .suffix = 0, .term = TERMINAL_TERMINOLOGY, },
    { .prefix = NULL, .suffix = 0, .term = TERMINAL_UNKNOWN, },
  }, *xtv;
  for(xtv = xtvers ; xtv->prefix ; ++xtv){
    if(strncmp(xtversion, xtv->prefix, strlen(xtv->prefix)) == 0){
      if(extract_xtversion(ictx, xtversion + strlen(xtv->prefix), xtv->suffix) == 0){
        loginfo("found terminal type %d version %s", xtv->term, ictx->initdata->version);
        ictx->initdata->qterm = xtv->term;
      }else{
        free(xtversion);
        return 2;
      }
      break;
    }
  }
  if(xtv->prefix == NULL){
    logwarn("unknown xtversion [%s]", xtversion);
  }
  free(xtversion);
  return 2;
}

// precondition: s starts with two hex digits, the first of which is not
// greater than 7.
static inline char
toxdigit(const char* s){
  char c = isalpha(*s) ? tolower(*s) - 'a' + 10 : *s - '0';
  c *= 16;
  ++s;
  c += isalpha(*s) ? tolower(*s) - 'a' + 10 : *s - '0';
  return c;
}

// on success, the subsequent character is returned, and |key| and |val| have
// heap-allocated, decoded, nul-terminated copies of the appropriate input.
static const char*
gettcap(const char* s, char** key, char** val){
  const char* equals = s;
  // we don't want anything bigger than 7 in the first nibble
  unsigned firstnibble = true;
  while(*equals != '='){
    if(!isxdigit(*equals)){ // rejects a NUL byte
      logerror("bad key in %s", s);
      return NULL;
    }
    if(firstnibble && (!isdigit(*equals) || *equals - '0' >= 8)){
      logerror("bad key in %s", s);
      return NULL;
    }
    firstnibble = !firstnibble;
    ++equals;
  }
  if(equals - s == 0 || !firstnibble){
    logerror("bad key in %s", s);
    return NULL;
  }
  if((*key = malloc((equals - s) / 2 + 1)) == NULL){
    return NULL;
  }
  char* keytarg = *key;
  do{
    *keytarg = toxdigit(s);
    s += 2;
    ++keytarg;
  }while(*s != '=');
  *keytarg = '\0';
  ++equals; // now one past the equal sign
  const char *end = equals;
  firstnibble = true;
  while(*end != ';' && *end){
    if(!isxdigit(*end)){
      logerror("bad value in %s", s);
      goto valerr;
    }
    if(firstnibble && (!isdigit(*end) || *end - '0' >= 8)){
      logerror("bad value in %s", s);
      goto valerr;
    }
    firstnibble = !firstnibble;
    ++end;
  }
  if(end - equals == 0 || !firstnibble){
    logerror("bad value in %s", s);
    goto valerr;
  }
  if((*val = malloc((end - equals) / 2 + 1)) == NULL){
    goto valerr;
  }
  char* valtarg = *val;
  ++s;
  do{
    *valtarg = toxdigit(s);
    s += 2;
    ++valtarg;
  }while(s != end);
  *valtarg = '\0';
  loginfo("key: %s val: %s", *key, *val);
  return end;

valerr:
  free(*key);
  *key = NULL;
  return NULL;
}

// replace \E with actual 0x1b for use as a terminfo-like format string,
// writing in-place (and updating the nul terminator, if necessary).
// returns its input.
static inline char*
determinfo(char* old){
  bool escaped = false;
  char* targo = old;
  for(char* o = old ; *o ; ++o){
    if(escaped){
      if(*o == 'E'){
        *targo = 0x1b;
        ++targo;
      }else{
        *targo = '\\';
        ++targo;
        *targo = *o;
        ++targo;
      }
      escaped = false;
    }else if(*o == '\\'){
      escaped = true;
    }else{
      *targo = *o;
      ++targo;
    }
  }
  *targo = '\0';
  return old;
}

// XTGETTCAP responses are delimited by semicolons
static int
tcap_cb(inputctx* ictx){
  char* str = amata_next_string(&ictx->amata, "\x1bP1+r");
  if(str == NULL){
    return 2;
  }
  loginfo("xtgettcap [%s]", str);
  if(ictx->initdata == NULL){
    free(str);
    return 2;
  }
  const char* s = str;
  char* key;
  char* val;
  // answers are delimited with semicolons, hex-encoded, key=value
  while(*s && (s = gettcap(s, &val, &key)) ){
    if(strcmp(val, "TN") == 0){
      if(ictx->initdata->qterm == TERMINAL_UNKNOWN){
        if(strcmp(key, "xterm") == 0){
          ictx->initdata->qterm = TERMINAL_XTERM;
        }else if(strcmp(key, "mlterm") == 0){
          ictx->initdata->qterm = TERMINAL_MLTERM;
        }else if(strcmp(key, "xterm-kitty") == 0){
          ictx->initdata->qterm = TERMINAL_KITTY;
        }else if(strcmp(key, "xterm-256color") == 0){
          ictx->initdata->qterm = TERMINAL_XTERM;
        }else{
          logdebug("unknown terminal name %s", key);
        }
      }
    }else if(strcmp(val, "RGB") == 0){
      loginfo("got rgb (%s)", s);
      ictx->initdata->rgb = true;
    }else if(strcmp(val, "hpa") == 0){
      loginfo("got hpa (%s)", key);
      ictx->initdata->hpa = determinfo(key);
      key = NULL;
    }else{
      logwarn("unknown capability: %s", str);
    }
    free(val);
    free(key);
    if(*s == ';'){
      ++s;
    }
  }
  if(!s || *s){
    free(str);
    return -1;
  }
  free(str);
  return 2;
}

static int
tda_cb(inputctx* ictx){
  char* str = amata_next_string(&ictx->amata, "\x1bP!|");
  if(str == NULL){
    logwarn("empty ternary device attribute");
    return 2; // don't replay
  }
  if(ictx->initdata && ictx->initdata->qterm == TERMINAL_UNKNOWN){
    if(strcmp(str, "7E565445") == 0){ // "~VTE"
      ictx->initdata->qterm = TERMINAL_VTE;
    }else if(strcmp(str, "7E7E5459") == 0){ // "~~TY"
      ictx->initdata->qterm = TERMINAL_TERMINOLOGY;
    }else if(strcmp(str, "464F4F54") == 0){ // "FOOT"
      ictx->initdata->qterm = TERMINAL_FOOT;
    }else if(strcmp(str, "7E4B4445") == 0){
      ictx->initdata->qterm = TERMINAL_KONSOLE;
    }
    loginfo("got TDA: %s, terminal type %d", str, ictx->initdata->qterm);
  }
  free(str);
  return 2;
}

static int
build_cflow_automaton(inputctx* ictx){
  // syntax: literals are matched. \N is a numeric. \D is a drain (Kleene
  // closure). \S is a ST-terminated string. this working is very dependent on
  // order, and very delicate! hands off!
  const struct {
    const char* cflow;
    triefunc fxn;
  } csis[] = {
    // CSI (\e[)
    { "[E", simple_cb_begin, },
    { "[<\\N;\\N;\\NM", mouse_press_cb, },
    { "[<\\N;\\N;\\Nm", mouse_release_cb, },
    // technically these must begin with "4" or "8"; enforce in callbacks
    { "[\\N;\\N;\\Nt", geom_cb, },
    { "[\\Nu", kitty_cb_simple, },
    { "[\\N;\\N~", wezterm_cb, },
    { "[\\N;\\Nu", kitty_cb, },
    { "[\\N;\\N:\\Nu", kitty_cb_complex, },
    { "[\\N;\\N;\\N~", xtmodkey_cb, },
    { "[\\N;\\N:\\N~", kitty_cb_functional, },
    { "[1;\\NP", legacy_cb_f1, },
    { "[1;\\NQ", legacy_cb_f2, },
    { "[1;\\NS", legacy_cb_f4, },
    { "[1;\\ND", legacy_cb_left, },
    { "[1;\\NC", legacy_cb_right, },
    { "[1;\\NB", legacy_cb_down, },
    { "[1;\\NA", legacy_cb_up, },
    { "[1;\\NE", legacy_cb_begin, },
    { "[1;\\NF", legacy_cb_end, },
    { "[1;\\NH", legacy_cb_home, },
    { "[1;\\N:\\NP", kitty_cb_f1, },
    { "[1;\\N:\\NQ", kitty_cb_f2, },
    { "[1;\\N:\\NR", kitty_cb_f3, },
    { "[1;\\N:\\NS", kitty_cb_f4, },
    { "[1;\\N:\\ND", kitty_cb_left, },
    { "[1;\\N:\\NC", kitty_cb_right, },
    { "[1;\\N:\\NB", kitty_cb_down, },
    { "[1;\\N:\\NA", kitty_cb_up, },
    { "[1;\\N:\\NE", kitty_cb_begin, },
    { "[1;\\N:\\NF", kitty_cb_end, },
    { "[1;\\N:\\NH", kitty_cb_home, },
    { "[?\\Nu", kitty_keyboard_cb, },
    { "[?1016;\\N$y", decrpm_pixelmice, },
    { "[?2026;\\N$y", decrpm_asu_cb, },
    { "[\\N;\\NR", cursor_location_cb, },
    { "[?1;1S", NULL, }, // negative cregs XTSMGRAPHICS
    { "[?1;2S", NULL, }, // negative cregs XTSMGRAPHICS
    { "[?1;3S", NULL, }, // negative cregs XTSMGRAPHICS
    { "[?1;3;S", NULL, }, // iterm2 negative cregs XTSMGRAPHICS
    { "[?1;3;0S", NULL, }, // negative cregs XTSMGRAPHICS
    { "[?2;1S", NULL, }, // negative pixels XTSMGRAPHICS
    { "[?2;2S", NULL, }, // negative pixels XTSMGRAPHICS
    { "[?2;3S", NULL, }, // negative pixels XTSMGRAPHICS
    { "[?2;3;S", NULL, }, // iterm2 negative pixels XTSMGRAPHICS
    { "[?2;3;0S", NULL, }, // negative pixels XTSMGRAPHICS
    { "[?6c", da1_vt102_cb, },   // CSI ? 6 c ("VT102")
    { "[?7c", da1_cb, },   // CSI ? 7 c ("VT131")
    { "[?1;0c", da1_cb, }, // CSI ? 1 ; 0 c ("VT101 with No Options")
    { "[?1;2c", da1_cb, }, // CSI ? 1 ; 2 c ("VT100 with Advanced Video Option")
    { "[?4;6c", da1_cb, }, // CSI ? 4 ; 6 c ("VT132 with Advanced Video and Graphics")
    // CSI ? 1 2 ; Ps c ("VT125")
    // CSI ? 6 0 ; Ps c (kmscon)
    // CSI ? 6 2 ; Ps c ("VT220")
    // CSI ? 6 3 ; Ps c ("VT320")
    // CSI ? 6 4 ; Ps c ("VT420")
    // CSI ? 6 5 ; Ps c (WezTerm, VT5xx?)
    { "[?\\N;\\Dc", da1_attrs_cb, },
    { "[?1;0;\\NS", xtsmgraphics_cregs_cb, },
    { "[?2;0;\\N;\\NS", xtsmgraphics_sixel_cb, },
    { "[>83;\\N;0c", da2_screen_cb, },
    { "[>\\N;\\N;\\Nc", da2_cb, },
    { "[=\\Sc", wezterm_tda_cb, }, // CSI da3 form as issued by WezTerm
    // DCS (\eP...ST)
    { "P0+\\S", NULL, }, // negative XTGETTCAP
    { "P1+r\\S", tcap_cb, }, // positive XTGETTCAP
    { "P!|\\S", tda_cb, }, // DCS da3 form used by XTerm
    { "P>|\\S", xtversion_cb, },
    // OSC (\e_...ST)
    { "_G\\S", kittygraph_cb, },
    // a mystery to everyone!
    { "]10;rgb:\\S", fgdef_cb, },
    { "]11;rgb:\\S", bgdef_cb, },
    { NULL, NULL, },
  }, *csi;
  for(csi = csis ; csi->cflow ; ++csi){
    if(inputctx_add_cflow(&ictx->amata, csi->cflow, csi->fxn)){
      logerror("failed adding %p via %s", csi->fxn, csi->cflow);
      return -1;
    }
    loginfo("added %p via %s", csi->fxn, csi->cflow);
  }
  if(ictx->ti->qterm == TERMINAL_RXVT){
    if(inputctx_add_cflow(&ictx->amata, "]4;\\N;rgb:\\R", palette_cb)){
      logerror("failed adding palette_cb");
      return -1;
    }
  }else{
    if(inputctx_add_cflow(&ictx->amata, "]4;\\N;rgb:\\S", palette_cb)){
      logerror("failed adding palette_cb");
      return -1;
    }
    // handle old-style contour responses, though we can't make use of them
    if(inputctx_add_cflow(&ictx->amata, "]4;rgb:\\S", palette_cb)){
      logerror("failed adding palette_cb");
      return -1;
    }
  }
  return 0;
}

static void
closepipe(ipipe p){
#ifndef __MINGW32__
  if(p >= 0){
    close(p);
  }
#else
  if(p){
    CloseHandle(p);
  }
#endif
}

static void
endpipes(ipipe pipes[static 2]){
  closepipe(pipes[0]);
  closepipe(pipes[1]);
}

// only linux and freebsd13+ have eventfd(), so we'll fall back to pipes sigh.
static int
getpipes(ipipe pipes[static 2]){
#ifndef __MINGW32__
#ifndef __APPLE__
  if(pipe2(pipes, O_CLOEXEC | O_NONBLOCK)){
    logerror("couldn't get pipes (%s)", strerror(errno));
    return -1;
  }
#else
  if(pipe(pipes)){
    logerror("couldn't get pipes (%s)", strerror(errno));
    return -1;
  }
  if(set_fd_cloexec(pipes[0], 1, NULL) || set_fd_nonblocking(pipes[0], 1, NULL)){
    logerror("couldn't prep pipe[0] (%s)", strerror(errno));
    endpipes(pipes);
    return -1;
  }
  if(set_fd_cloexec(pipes[1], 1, NULL) || set_fd_nonblocking(pipes[1], 1, NULL)){
    logerror("couldn't prep pipe[1] (%s)", strerror(errno));
    endpipes(pipes);
    return -1;
  }
#endif
#else // windows
  if(!CreatePipe(&pipes[0], &pipes[1], NULL, BUFSIZ)){
    logerror("couldn't get pipes");
    return -1;
  }
#endif
  return 0;
}

static inline inputctx*
create_inputctx(tinfo* ti, FILE* infp, int lmargin, int tmargin, int rmargin,
                int bmargin, ncsharedstats* stats, unsigned drain,
                int linesigs_enabled){
  bool sent_queries = (ti->ttyfd >= 0) ? true : false;
  inputctx* i = malloc(sizeof(*i));
  if(i){
    i->csize = 64;
    if( (i->csrs = malloc(sizeof(*i->csrs) * i->csize)) ){
      i->isize = BUFSIZ;
      if( (i->inputs = malloc(sizeof(*i->inputs) * i->isize)) ){
        if(pthread_mutex_init(&i->ilock, NULL) == 0){
          if(pthread_condmonotonic_init(&i->icond) == 0){
            if(pthread_mutex_init(&i->clock, NULL) == 0){
              if(pthread_condmonotonic_init(&i->ccond) == 0){
                if((i->stdinfd = fileno(infp)) >= 0){
                  if( (i->initdata = malloc(sizeof(*i->initdata))) ){
                    if(getpipes(i->readypipes) == 0){
                      if(getpipes(i->ipipes) == 0){
                        memset(&i->amata, 0, sizeof(i->amata));
                        if(prep_special_keys(i) == 0){
                          if(set_fd_nonblocking(i->stdinfd, 1, &ti->stdio_blocking_save) == 0){
                            i->termfd = tty_check(i->stdinfd) ? -1 : get_tty_fd(infp);
                            memset(i->initdata, 0, sizeof(*i->initdata));
                            if(sent_queries){
                              i->coutstanding = 1; // one in initial request set
                              i->initdata->qterm = ti->qterm;
                              i->initdata->cursory = -1;
                              i->initdata->cursorx = -1;
                              i->initdata->maxpaletteread = -1;
                              i->initdata->kbdlevel = UINT_MAX;
                            }else{
                              free(i->initdata);
                              i->initdata = NULL;
                              i->coutstanding = 0;
                            }
                            i->kittykbd = 0;
                            i->iread = i->iwrite = i->ivalid = 0;
                            i->cread = i->cwrite = i->cvalid = 0;
                            i->initdata_complete = NULL;
                            i->stats = stats;
                            i->ti = ti;
                            i->stdineof = 0;
#ifdef __MINGW32__
                            i->stdinhandle = ti->inhandle;
#endif
                            i->ibufvalid = 0;
                            i->linesigs = linesigs_enabled;
                            i->tbufvalid = 0;
                            i->midescape = 0;
                            i->lmargin = lmargin;
                            i->tmargin = tmargin;
                            i->rmargin = rmargin;
                            i->bmargin = bmargin;
                            i->drain = drain;
                            i->failed = false;
                            logdebug("input descriptors: %d/%d", i->stdinfd, i->termfd);
                            return i;
                          }
                        }
                        input_free_esctrie(&i->amata);
                      }
                      endpipes(i->ipipes);
                    }
                    endpipes(i->readypipes);
                  }
                  free(i->initdata);
                }
                pthread_cond_destroy(&i->ccond);
              }
              pthread_mutex_destroy(&i->clock);
            }
            pthread_cond_destroy(&i->icond);
          }
          pthread_mutex_destroy(&i->ilock);
        }
        free(i->inputs);
      }
      free(i->csrs);
    }
    free(i);
  }
  return NULL;
}

static inline void
free_inputctx(inputctx* i){
  if(i){
    // we *do not* own stdinfd; don't close() it! we do own termfd.
    if(i->termfd >= 0){
      close(i->termfd);
    }
    pthread_mutex_destroy(&i->ilock);
    pthread_cond_destroy(&i->icond);
    pthread_mutex_destroy(&i->clock);
    pthread_cond_destroy(&i->ccond);
    input_free_esctrie(&i->amata);
    // do not kill the thread here, either.
    if(i->initdata){
      free(i->initdata->version);
      free(i->initdata);
    }else if(i->initdata_complete){
      free(i->initdata_complete->version);
      free(i->initdata_complete);
    }
    endpipes(i->readypipes);
    endpipes(i->ipipes);
    free(i->inputs);
    free(i->csrs);
    free(i);
  }
}

// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
static int
prep_kitty_special_keys(inputctx* ictx){
  // we do not list here those already handled by prep_windows_special_keys()
  static const struct {
    const char* esc;
    uint32_t key;
    unsigned modifiers;
  } keys[] = {
    { .esc = "\x1b[P", .key = NCKEY_F01, },
    { .esc = "\x1b[Q", .key = NCKEY_F02, },
    { .esc = "\x1b[R", .key = NCKEY_F03, },
    { .esc = "\x1b[S", .key = NCKEY_F04, },
    { .esc = "\x1b[127;2u", .key = NCKEY_BACKSPACE,
      .modifiers = NCKEY_MOD_SHIFT, },
    { .esc = "\x1b[127;3u", .key = NCKEY_BACKSPACE,
      .modifiers = NCKEY_MOD_ALT, },
    { .esc = "\x1b[127;5u", .key = NCKEY_BACKSPACE,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = NULL, .key = 0, },
  }, *k;
  for(k = keys ; k->esc ; ++k){
    if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key, k->modifiers)){
      return -1;
    }
  }
  loginfo("added all kitty special keys");
  return 0;
}

// add the hardcoded windows input sequences to ti->input. should only
// be called after verifying that this is TERMINAL_MSTERMINAL.
static int
prep_windows_special_keys(inputctx* ictx){
  // here, lacking terminfo, we hardcode the sequences. they can be found at
  // https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
  // under the "Input Sequences" heading.
  static const struct {
    const char* esc;
    uint32_t key;
    unsigned modifiers;
  } keys[] = {
    { .esc = "\x1b[A", .key = NCKEY_UP, },
    { .esc = "\x1b[B", .key = NCKEY_DOWN, },
    { .esc = "\x1b[C", .key = NCKEY_RIGHT, },
    { .esc = "\x1b[D", .key = NCKEY_LEFT, },
    { .esc = "\x1b[1;5A", .key = NCKEY_UP,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[1;5B", .key = NCKEY_DOWN,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[1;5C", .key = NCKEY_RIGHT,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[1;5D", .key = NCKEY_LEFT,
      .modifiers = NCKEY_MOD_CTRL, },
    { .esc = "\x1b[H", .key = NCKEY_HOME, },
    { .esc = "\x1b[F", .key = NCKEY_END, },
    { .esc = "\x1b[2~", .key = NCKEY_INS, },
    { .esc = "\x1b[3~", .key = NCKEY_DEL, },
    { .esc = "\x1b[5~", .key = NCKEY_PGUP, },
    { .esc = "\x1b[6~", .key = NCKEY_PGDOWN, },
    { .esc = "\x1bOP", .key = NCKEY_F01, },
    { .esc = "\x1bOQ", .key = NCKEY_F02, },
    { .esc = "\x1bOR", .key = NCKEY_F03, },
    { .esc = "\x1bOS", .key = NCKEY_F04, },
    { .esc = "\x1b[15~", .key = NCKEY_F05, },
    { .esc = "\x1b[17~", .key = NCKEY_F06, },
    { .esc = "\x1b[18~", .key = NCKEY_F07, },
    { .esc = "\x1b[19~", .key = NCKEY_F08, },
    { .esc = "\x1b[20~", .key = NCKEY_F09, },
    { .esc = "\x1b[21~", .key = NCKEY_F10, },
    { .esc = "\x1b[23~", .key = NCKEY_F11, },
    { .esc = "\x1b[24~", .key = NCKEY_F12, },
    { .esc = NULL, .key = 0, },
  }, *k;
  for(k = keys ; k->esc ; ++k){
    if(inputctx_add_input_escape(&ictx->amata, k->esc, k->key, k->modifiers)){
      return -1;
    }
    logdebug("added %s %u", k->esc, k->key);
  }
  loginfo("added all windows special keys");
  return 0;
}

static int
prep_all_keys(inputctx* ictx){
  if(prep_windows_special_keys(ictx)){
    return -1;
  }
  if(prep_kitty_special_keys(ictx)){
    return -1;
  }
  if(prep_xtmodkeys(ictx)){
    return -1;
  }
  return 0;
}

// populate |buf| with any new data from the specified file descriptor |fd|.
static void
read_input_nblock(int fd, unsigned char* buf, size_t buflen, int *bufused,
                  unsigned* goteof){
  if(fd < 0){
    return;
  }
  size_t space = buflen - *bufused;
  if(space == 0){
    return;
  }
  ssize_t r = read(fd, buf + *bufused, space);
  if(r <= 0){
    if(r < 0 && (errno != EAGAIN && errno != EBUSY && errno == EWOULDBLOCK)){
      logwarn("couldn't read from %d (%s)", fd, strerror(errno));
    }else{
      if(r < 0){
        logerror("error reading from %d (%s)", fd, strerror(errno));
      }else{
        logwarn("got EOF on %d", fd);
      }
      if(goteof){
        *goteof = 1;
      }
    }
    return;
  }
  *bufused += r;
  space -= r;
  loginfo("read %" PRIdPTR "B from %d (%" PRIuPTR "B left)", r, fd, space);
}

// are terminal and stdin distinct for this inputctx?
static inline bool
ictx_independent_p(const inputctx* ictx){
  return ictx->termfd >= 0;
}

// try to lex a single control sequence off of buf. return the number of bytes
// consumed if we do so. otherwise, return the negative number of bytes
// examined. set ictx->midescape if we're uncertain. we preserve a->used,
// a->state, etc. across runs to avoid reprocessing. buf is almost certainly
// *not* NUL-terminated.
//
// our rule is: an escape must arrive as a single unit to be interpreted as
// an escape. this is most relevant for Alt+keypress (Esc followed by the
// character), which is ambiguous with regards to pressing 'Escape' followed
// by some other character. if they arrive together, we consider it to be
// the escape. we might need to allow more than one process_escape call,
// however, in case the escape ended the previous read buffer.
//  precondition: buflen >= 1. precondition: buf[0] == 0x1b.
static int
process_escape(inputctx* ictx, const unsigned char* buf, int buflen){
  assert(ictx->amata.used <= buflen);
  while(ictx->amata.used < buflen){
    unsigned char candidate = buf[ictx->amata.used++];
    unsigned used = ictx->amata.used;
    if(candidate >= 0x80){
      ictx->amata.used = 0;
      return -(used - 1);
    }
    // an escape always resets the trie (unless we're in the middle of an
    // ST-terminated string), as does a NULL transition.
    if(candidate == NCKEY_ESC && !ictx->amata.instring){
      ictx->amata.matchstart = buf + ictx->amata.used - 1;
      ictx->amata.state = ictx->amata.escapes;
      logtrace("initialized automaton to %u", ictx->amata.state);
      ictx->amata.used = 1;
      if(used > 1){ // we got reset; replay as input
        return -(used - 1);
      }
      // validated first byte as escape! keep going. otherwise, check trie.
      // we can safely check trie[candidate] above because we are either coming
      // off the initial node, which definitely has a valid ->trie, or we're
      // coming from a transition, where ictx->triepos->trie is checked below.
    }else{
      ncinput ni = {0};
      int w = walk_automaton(&ictx->amata, ictx, candidate, &ni);
      logdebug("walk result on %u (%c): %d %u", candidate,
               isprint(candidate) ? candidate : ' ', w, ictx->amata.state);
      if(w > 0){
        if(ni.id){
          load_ncinput(ictx, &ni);
        }
        ictx->amata.used = 0;
        return used;
      }else if(w < 0){
        // all inspected characters are invalid; return full negative "used"
        ictx->amata.used = 0;
        return -used;
      }
    }
  }
  // we exhausted input without knowing whether or not this is a valid control
  // sequence; we're still on-trie, and need more (immediate) input.
  ictx->midescape = 1;
  return -ictx->amata.used;
}

// process as many control sequences from |buf|, having |bufused| bytes,
// as we can. anything not a valid control sequence is dropped. this text
// needn't be valid UTF-8. this is always called on tbuf; if we find bulk data
// here, we need replay it into ibuf (assuming that there's room).
static void
process_escapes(inputctx* ictx, unsigned char* buf, int* bufused){
  int offset = 0;
  while(*bufused){
    int consumed = process_escape(ictx, buf + offset, *bufused);
    // negative |consumed| means either that we're not sure whether it's an
    // escape, or it definitely is not.
    if(consumed < 0){
      // if midescape is not set, the negative return means invalid escape.
      // replay it to the bulk input buffer; our automaton will have been reset.
      if(!ictx->midescape){
        consumed = -consumed;
        int available = sizeof(ictx->ibuf) - ictx->ibufvalid;
        if(available){
          if(available > consumed){
            available = consumed;
          }
          logwarn("replaying %dB of %dB to ibuf", available, consumed);
          memcpy(ictx->ibuf + ictx->ibufvalid, buf + offset, available);
          ictx->ibufvalid += available;
        }
        offset += consumed;
        ictx->midescape = 0;
        *bufused -= consumed;
        assert(0 <= *bufused);
      }else{
        break;
      }
    }
    *bufused -= consumed;
    offset += consumed;
    assert(0 <= *bufused);
  }
  // move any leftovers to the front; only happens if we fill output queue,
  // or ran out of input data mid-escape
  if(*bufused){
    ictx->amata.matchstart = buf;
    memmove(buf, buf + offset, *bufused);
  }
}

// precondition: buflen >= 1. attempts to consume UTF8 input from buf. the
// expected length of a UTF8 character can be determined from its first byte.
// if we don't have that much data, return 0 and read more. if we determine
// an error, return -1 to consume 1 byte, restarting the UTF8 lex on the next
// byte. on a valid UTF8 character, set up the ncinput and return its length.
static int
process_input(const unsigned char* buf, int buflen, ncinput* ni){
  assert(1 <= buflen);
  memset(ni, 0, sizeof(*ni));
  const int cpointlen = utf8_codepoint_length(*buf);
  if(cpointlen <= 0){
    logwarn("invalid UTF8 initiator on input (0x%02x)", *buf);
    return -1;
  }else if(cpointlen == 1){ // pure ascii can't show up mid-utf8-character
    ni->id = buf[0];
    return 1;
  }
  if(cpointlen > buflen){
    logwarn("utf8 character (%dB) broken across read", cpointlen);
    return 0; // need read more data; we don't have the complete character
  }
  wchar_t w;
  mbstate_t mbstate = {0};
//fprintf(stderr, "CANDIDATE: %d cpointlen: %zu cpoint: %d\n", candidate, cpointlen, cpoint[cpointlen]);
  // FIXME how the hell does this work with 16-bit wchar_t?
  size_t r = mbrtowc(&w, (const char*)buf, cpointlen, &mbstate);
  if(r == (size_t)-1 || r == (size_t)-2){
    logerror("invalid utf8 prefix (%dB) on input", cpointlen);
    return -1;
  }
  ni->id = w;
  return cpointlen;
}

// precondition: buflen >= 1. gets an ncinput prepared by process_input, and
// sticks that into the bulk queue.
static int
process_ncinput(inputctx* ictx, const unsigned char* buf, int buflen){
  ncinput ni;
  int r = process_input(buf, buflen, &ni);
  if(r > 0){
    load_ncinput(ictx, &ni);
  }else if(r < 0){
    inc_input_errors(ictx);
    r = 1; // we want to consume a single byte, upstairs
  }
  return r;
}

// handle redirected input (i.e. not from our connected terminal). process as
// much bulk UTF-8 input as we can, knowing it to be free of control sequences.
// anything not a valid UTF-8 character is dropped.
static void
process_bulk(inputctx* ictx, unsigned char* buf, int* bufused){
  int offset = 0;
  while(*bufused){
    bool noroom = false;
    pthread_mutex_lock(&ictx->ilock);
    if(ictx->ivalid == ictx->isize){
      noroom = true;
    }
    pthread_mutex_unlock(&ictx->ilock);
    if(noroom){
      break;
    }
    int consumed = process_ncinput(ictx, buf + offset, *bufused);
    if(consumed <= 0){
      break;
    }
    *bufused -= consumed;
    offset += consumed;
  }
  // move any leftovers to the front
  if(*bufused){
    memmove(buf, buf + offset, *bufused);
  }
}

// process as much mixed input as we can. we might find UTF-8 bulk input and
// control sequences mixed (though each individual character/sequence ought be
// contiguous). known control sequences are removed for internal processing.
// everything else will be handed up to the client (assuming it to be valid
// UTF-8).
static void
process_melange(inputctx* ictx, const unsigned char* buf, int* bufused){
  int offset = 0;
  while(*bufused){
    logdebug("input %d (%u)/%d [0x%02x] (%c)", offset, ictx->amata.used,
             *bufused, buf[offset], isprint(buf[offset]) ? buf[offset] : ' ');
    int consumed = 0;
    if(buf[offset] == '\x1b'){
      consumed = process_escape(ictx, buf + offset, *bufused);
      if(consumed < 0){
        if(ictx->midescape){
          if(*bufused != -consumed){
            // not at the end; treat it as input. no need to move between
            // buffers; simply ensure we process it as input, and don't mark
            // anything as consumed.
            ictx->midescape = 0;
          }
        }
      }
    }
    // don't process as input only if we just read a valid control character,
    // or if we need to read more to determine what it is.
    if(consumed <= 0 && !ictx->midescape){
      consumed = process_ncinput(ictx, buf + offset, *bufused);
    }
    if(consumed < 0){
      break;
    }
    *bufused -= consumed;
    offset += consumed;
  }
  handoff_initial_responses_late(ictx);
}

// walk the matching automaton from wherever we were.
static void
process_ibuf(inputctx* ictx){
  if(resize_seen){
    ncinput tni = {
      .id = NCKEY_RESIZE,
    };
    load_ncinput(ictx, &tni);
    resize_seen = 0;
  }
  if(cont_seen){
    ncinput tni = {
      .id = NCKEY_SIGNAL,
    };
    load_ncinput(ictx, &tni);
    cont_seen = 0;
  }
  if(ictx->tbufvalid){
    // we could theoretically do this in parallel with process_bulk, but it
    // hardly seems worthwhile without breaking apart the fetches of input.
    process_escapes(ictx, ictx->tbuf, &ictx->tbufvalid);
    handoff_initial_responses_late(ictx);
  }
  if(ictx->ibufvalid){
    if(ictx_independent_p(ictx)){
      process_bulk(ictx, ictx->ibuf, &ictx->ibufvalid);
    }else{
      int valid = ictx->ibufvalid;
      process_melange(ictx, ictx->ibuf, &ictx->ibufvalid);
      // move any leftovers to the front
      if(ictx->ibufvalid){
        memmove(ictx->ibuf, ictx->ibuf + valid - ictx->ibufvalid, ictx->ibufvalid);
        if(ictx->amata.matchstart){
          ictx->amata.matchstart = ictx->ibuf;
        }
      }
    }
  }
}

int ncinput_shovel(inputctx* ictx, const void* buf, int len){
  process_melange(ictx, buf, &len);
  if(len){
    logwarn("dropping %d byte%s", len, len == 1 ? "" : "s");
    inc_input_errors(ictx);
  }
  return 0;
}

// here, we always block for an arbitrarily long time, or not at all,
// doing the latter only when ictx->midescape is set. |rtfd| and/or |rifd|
// are set high iff they are ready for reading, and otherwise cleared.
static int
block_on_input(inputctx* ictx, unsigned* rtfd, unsigned* rifd){
  logtrace("blocking on input availability");
  *rtfd = *rifd = 0;
  unsigned nonblock = ictx->midescape;
  if(nonblock){
    loginfo("nonblocking read to check for completion");
    ictx->midescape = 0;
  }
#ifdef __MINGW32__
  int timeoutms = nonblock ? 0 : -1;
  DWORD ncount = 0;
  HANDLE handles[2];
  if(!ictx->stdineof){
    if(ictx->ibufvalid != sizeof(ictx->ibuf)){
      handles[ncount++] = ictx->stdinhandle;
    }
  }
  if(ncount == 0){
    handles[ncount++] = ictx->ipipes[0];
  }
  DWORD d = WaitForMultipleObjects(ncount, handles, false, timeoutms);
  if(d == WAIT_TIMEOUT){
    return 0;
  }else if(d == WAIT_FAILED){
    return -1;
  }else if(d - WAIT_OBJECT_0 == 0){
    *rifd = 1;
    return 1;
  }
  return -1;
#else
  int inevents = POLLIN;
#ifdef POLLRDHUP
  inevents |= POLLRDHUP;
#endif
  struct pollfd pfds[2];
  int pfdcount = 0;
  if(!ictx->stdineof){
    if(ictx->ibufvalid != sizeof(ictx->ibuf)){
      pfds[pfdcount].fd = ictx->stdinfd;
      pfds[pfdcount].events = inevents;
      pfds[pfdcount].revents = 0;
      ++pfdcount;
    }
  }
  if(pfdcount == 0){
    loginfo("output queues full; blocking on ipipes");
    pfds[pfdcount].fd = ictx->ipipes[0];
    pfds[pfdcount].events = inevents;
    pfds[pfdcount].revents = 0;
    ++pfdcount;
  }
  if(ictx->termfd >= 0){
    pfds[pfdcount].fd = ictx->termfd;
    pfds[pfdcount].events = inevents;
    pfds[pfdcount].revents = 0;
    ++pfdcount;
  }
  logtrace("waiting on %d fds (ibuf: %u/%"PRIuPTR")", pfdcount, ictx->ibufvalid, sizeof(ictx->ibuf));
  sigset_t smask;
  sigfillset(&smask);
  sigdelset(&smask, SIGCONT);
  sigdelset(&smask, SIGWINCH);
#ifdef SIGTHR
  // freebsd uses SIGTHR for thread cancellation; need this to ensure wakeup
  // on exit (in cancel_and_join()).
  sigdelset(&smask, SIGTHR);
#endif
  int events;
#if defined(__APPLE__) || defined(__MINGW32__)
  int timeoutms = nonblock ? 0 : -1;
  while((events = poll(pfds, pfdcount, timeoutms)) < 0){ // FIXME smask?
#else
  struct timespec ts = { .tv_sec = 0, .tv_nsec = 0, };
  struct timespec* pts = nonblock ? &ts : NULL;
  while((events = ppoll(pfds, pfdcount, pts, &smask)) < 0){
#endif
    if(errno == EINTR){
      loginfo("interrupted by signal");
      return resize_seen;
    }else if(errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){
      logerror("error polling (%s)", strerror(errno));
      return -1;
    }
  }
  loginfo("poll returned %d", events);
  pfdcount = 0;
  while(events){
    if(pfds[pfdcount].revents){
      if(pfds[pfdcount].fd == ictx->stdinfd){
        *rifd = 1;
      }else if(pfds[pfdcount].fd == ictx->termfd){
        *rtfd = 1;
      }else if(pfds[pfdcount].fd == ictx->ipipes[0]){
        char c;
        while(read(ictx->ipipes[0], &c, sizeof(c)) == 1){
          // FIXME accelerate?
        }
      }
      --events;
    }
    ++pfdcount;
  }
  loginfo("got events: %c%c", *rtfd ? 'T' : 't', *rifd ? 'I' : 'i');
  return events;
#endif
}

// populate the ibuf with any new data, up through its size, but do not block.
// don't loop around this call without some kind of readiness notification.
static void
read_inputs_nblock(inputctx* ictx){
  unsigned rtfd, rifd;
  block_on_input(ictx, &rtfd, &rifd);
  // first we read from the terminal, if that's a distinct source.
  if(rtfd){
    read_input_nblock(ictx->termfd, ictx->tbuf, sizeof(ictx->tbuf),
                      &ictx->tbufvalid, NULL);
  }
  // now read bulk, possibly with term escapes intermingled within (if there
  // was not a distinct terminal source).
  if(rifd){
    unsigned eof = ictx->stdineof;
    read_input_nblock(ictx->stdinfd, ictx->ibuf, sizeof(ictx->ibuf),
                      &ictx->ibufvalid, &ictx->stdineof);
    // did we switch from non-EOF state to EOF? if so, mark us ready
    if(!eof && ictx->stdineof){
      // we hit EOF; write an event to the readiness fd
      mark_pipe_ready(ictx->readypipes);
      pthread_cond_broadcast(&ictx->icond);
    }
  }
}

static void*
input_thread(void* vmarshall){
  setup_alt_sig_stack();
  inputctx* ictx = vmarshall;
  if(prep_all_keys(ictx) || build_cflow_automaton(ictx)){
    ictx->failed = true;
    handoff_initial_responses_early(ictx);
    handoff_initial_responses_late(ictx);
  }
  for(;;){
    read_inputs_nblock(ictx);
    // process anything we've read
    process_ibuf(ictx);
  }
  return NULL;
}

int init_inputlayer(tinfo* ti, FILE* infp, int lmargin, int tmargin,
                    int rmargin, int bmargin, ncsharedstats* stats,
                    unsigned drain, int linesigs_enabled){
  inputctx* ictx = create_inputctx(ti, infp, lmargin, tmargin, rmargin,
                                   bmargin, stats, drain, linesigs_enabled);
  if(ictx == NULL){
    return -1;
  }
  if(pthread_create(&ictx->tid, NULL, input_thread, ictx)){
    free_inputctx(ictx);
    return -1;
  }
  ti->ictx = ictx;
  loginfo("spun up input thread");
  return 0;
}

int stop_inputlayer(tinfo* ti){
  int ret = 0;
  if(ti){
    // FIXME cancellation on shutdown does not yet work on windows #2192
#ifndef __MINGW32__
    if(ti->ictx){
      loginfo("tearing down input thread");
      ret |= cancel_and_join("input", ti->ictx->tid, NULL);
      ret |= set_fd_nonblocking(ti->ictx->stdinfd, ti->stdio_blocking_save, NULL);
      free_inputctx(ti->ictx);
      ti->ictx = NULL;
    }
#endif
  }
  return ret;
}

int inputready_fd(const inputctx* ictx){
#ifndef __MINGW32__
  return ictx->readypipes[0];
#else
  (void)ictx;
  logerror("readiness descriptor unavailable on windows");
  return -1;
#endif
}

static inline uint32_t
internal_get(inputctx* ictx, const struct timespec* ts, ncinput* ni){
  uint32_t id;
  if(ictx->drain){
    logerror("input is being drained");
    if(ni){
      memset(ni, 0, sizeof(*ni));
      ni->id = (uint32_t)-1;
    }
    return (uint32_t)-1;
  }
  pthread_mutex_lock(&ictx->ilock);
  while(!ictx->ivalid){
    if(ictx->stdineof){
      pthread_mutex_unlock(&ictx->ilock);
      logwarn("read eof on stdin");
      if(ni){
        memset(ni, 0, sizeof(*ni));
        ni->id = NCKEY_EOF;
      }
      return NCKEY_EOF;
    }
    if(ts == NULL){
      pthread_cond_wait(&ictx->icond, &ictx->ilock);
    }else{
      int r = pthread_cond_timedwait(&ictx->icond, &ictx->ilock, ts);
      if(r == ETIMEDOUT){
        pthread_mutex_unlock(&ictx->ilock);
        if(ni){
          memset(ni, 0, sizeof(*ni));
        }
        return 0;
      }else if(r < 0){
        inc_input_errors(ictx);
        if(ni){
          memset(ni, 0, sizeof(*ni));
          ni->id = (uint32_t)-1;
        }
        return (uint32_t)-1;
      }
    }
  }
  id = ictx->inputs[ictx->iread].id;
  if(ni){
    memcpy(ni, &ictx->inputs[ictx->iread], sizeof(*ni));
    if(notcurses_ucs32_to_utf8(&ni->id, 1, (unsigned char*)ni->utf8, sizeof(ni->utf8)) < 0){
      ni->utf8[0] = 0;
    }
  }
  if(++ictx->iread == ictx->isize){
    ictx->iread = 0;
  }
  bool sendsignal = false;
  if(ictx->ivalid-- == ictx->isize){
    sendsignal = true;
  }else{
    logtrace("draining event readiness pipe %d", ictx->ivalid);
#ifndef __MINGW32__
    char c;
    while(read(ictx->readypipes[0], &c, sizeof(c)) == 1){
      // FIXME accelerate?
    }
#else
    // we ought be draining this, but it breaks everything, as we can't easily
    // do nonblocking input from a pipe in windows, augh...
    // Ne pleure pas, Alfred! J'ai besoin de tout mon courage pour mourir a vingt ans!
    /*while(ReadFile(ictx->readypipes[0], &c, sizeof(c), NULL, NULL)){
      // FIXME accelerate?
    }*/
#endif
  }
  pthread_mutex_unlock(&ictx->ilock);
  if(sendsignal){
    mark_pipe_ready(ictx->ipipes);
  }
  return id;
}

// infp has already been set non-blocking
uint32_t notcurses_get(notcurses* nc, const struct timespec* absdl, ncinput* ni){
  uint32_t ret = internal_get(nc->tcache.ictx, absdl, ni);
  return ret;
}

// FIXME better performance if we move this within the locked area
int notcurses_getvec(notcurses* n, const struct timespec* absdl,
                     ncinput* ni, int vcount){
  for(int v = 0 ; v < vcount ; ++v){
    uint32_t u = notcurses_get(n, absdl, &ni[v]);
    if(u == (uint32_t)-1){
      if(v == 0){
        return -1;
      }
      return v;
    }else if(u == 0){
      return v;
    }
  }
  return vcount;
}

uint32_t ncdirect_get(ncdirect* n, const struct timespec* absdl, ncinput* ni){
  if(n->eof){
    logerror("already got EOF");
    return -1;
  }
  uint32_t r = internal_get(n->tcache.ictx, absdl, ni);
  if(r == NCKEY_EOF){
    n->eof = 1;
  }
  return r;
}

int get_cursor_location(inputctx* ictx, const char* u7, unsigned* y, unsigned* x){
  pthread_mutex_lock(&ictx->clock);
  while(ictx->cvalid == 0){
    if(ictx->coutstanding == 0){
      if(tty_emit(u7, ictx->ti->ttyfd)){
        pthread_mutex_unlock(&ictx->clock);
        return -1;
      }
      ++ictx->coutstanding;
    }
    pthread_cond_wait(&ictx->ccond, &ictx->clock);
  }
  const cursorloc* cloc = &ictx->csrs[ictx->cread];
  if(++ictx->cread == ictx->csize){
    ictx->cread = 0;
  }
  --ictx->cvalid;
  if(y){
    *y = cloc->y;
  }
  if(x){
    *x = cloc->x;
  }
  pthread_mutex_unlock(&ictx->clock);
  return 0;
}

// Disable signals originating from the terminal's line discipline, i.e.
// SIGINT (^C), SIGQUIT (^\), and SIGTSTP (^Z). They are enabled by default.
static int
linesigs_disable(tinfo* ti){
  if(!ti->ictx->linesigs){
    logwarn("linedisc signals already disabled");
  }
#ifndef __MINGW32__
  if(ti->ttyfd < 0){
    return 0;
  }
  struct termios tios;
  if(tcgetattr(ti->ttyfd, &tios)){
    logerror("Couldn't preserve terminal state for %d (%s)", ti->ttyfd, strerror(errno));
    return -1;
  }
  tios.c_lflag &= ~ISIG;
  if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
    logerror("Error disabling signals on %d (%s)", ti->ttyfd, strerror(errno));
    return -1;
  }
#else
  DWORD mode;
  if(!GetConsoleMode(ti->inhandle, &mode)){
    logerror("error acquiring input mode");
    return -1;
  }
  mode &= ~ENABLE_PROCESSED_INPUT;
  if(!SetConsoleMode(ti->inhandle, mode)){
    logerror("error setting input mode");
    return -1;
  }
#endif
  ti->ictx->linesigs = 0;
  loginfo("disabled line discipline signals");
  return 0;
}

int notcurses_linesigs_disable(notcurses* nc){
  return linesigs_disable(&nc->tcache);
}

static int
linesigs_enable(tinfo* ti){
  if(ti->ictx->linesigs){
    logwarn("linedisc signals already enabled");
  }
#ifndef __MINGW32__
  if(ti->ttyfd < 0){
    return 0;
  }
  struct termios tios;
  if(tcgetattr(ti->ttyfd, &tios)){
    logerror("couldn't preserve terminal state for %d (%s)", ti->ttyfd, strerror(errno));
    return -1;
  }
  tios.c_lflag |= ISIG;
  if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
    logerror("error disabling signals on %d (%s)", ti->ttyfd, strerror(errno));
    return -1;
  }
#else
  DWORD mode;
  if(!GetConsoleMode(ti->inhandle, &mode)){
    logerror("error acquiring input mode");
    return -1;
  }
  mode |= ENABLE_PROCESSED_INPUT;
  if(!SetConsoleMode(ti->inhandle, mode)){
    logerror("error setting input mode");
    return -1;
  }
#endif
  ti->ictx->linesigs = 1;
  loginfo("enabled line discipline signals");
  return 0;
}

// Restore signals originating from the terminal's line discipline, i.e.
// SIGINT (^C), SIGQUIT (^\), and SIGTSTP (^Z), if disabled.
int notcurses_linesigs_enable(notcurses* n){
  return linesigs_enable(&n->tcache);
}

struct initial_responses* inputlayer_get_responses(inputctx* ictx){
  struct initial_responses* iresp;
  pthread_mutex_lock(&ictx->ilock);
  while(ictx->initdata || !ictx->initdata_complete){
    pthread_cond_wait(&ictx->icond, &ictx->ilock);
  }
  iresp = ictx->initdata_complete;
  ictx->initdata_complete = NULL;
  pthread_mutex_unlock(&ictx->ilock);
  if(ictx->failed){
    logpanic("aborting after automaton construction failure");
    free(iresp);
    return NULL;
  }
  return iresp;
}
