Repository: dankamongmen/notcurses (language: python)

Issue:
we need our own input thread
See #2131 for some motivation regarding the Kitty protocol, which inhibits `SIGINT` and friends. We're at the point where we need our own input thread. This would simplify a lot of code, especially cursor location report handling (we'd no longer need the dubious `cursor_location_simple()`). It's basically necessary for anything where *we* need respond to input rather than the client. This includes at least synthesizing signals (which ought only be done if line discipline signals have not been disabled) and cursor location reports.

This would be a big change for 2.4.0, and maybe it's better for 3.0.0? But right now we have #2131 scheduled for 2.4.0, and we need this to accomplish #2131...alright, bumping #2131 back to 3.0.0.
we need our own input thread
See #2131 for some motivation regarding the Kitty protocol, which inhibits `SIGINT` and friends. We're at the point where we need our own input thread. This would simplify a lot of code, especially cursor location report handling (we'd no longer need the dubious `cursor_location_simple()`). It's basically necessary for anything where *we* need respond to input rather than the client. This includes at least synthesizing signals (which ought only be done if line discipline signals have not been disabled) and cursor location reports.

This would be a big change for 2.4.0, and maybe it's better for 3.0.0? But right now we have #2131 scheduled for 2.4.0, and we need this to accomplish #2131...alright, bumping #2131 back to 3.0.0.


Discussion / hints:
so what's this thread doing?

it's got two output queues, each with a mutex and condvar: one of cursor location reports (and other future messages to us from the terminal), and one of processed user input (`ncinput`s). we're constantly poll()ing on either one or two descriptors (on Windows, we're using `WaitForMultipleObjects()` on the input handle). whenever there's data available, we read it in a large burst.

all lexing state must be in a self-contained struct carried across reads, so that we can handle mutlibyte sequences (escapes and/or UTF-8) that cross a read boundary.

at each lexing segmentation (end of a UTF8 character or escape), add to the appropriate queue, and signal its condvar.

if an output queue is full, drop its source from the poll. keep looking at any other sources. otherwise, block on a condvar. we'll need a pipe for each queue as well, and we'll want to poll on *that*.

on read, lock the queue. extract the next item if there are any items, and unlock. otherwise, block on a condvar. if the queue was full when you read it, signal the general condvar and write to your pipe.

when we want a cursor report now, we can just freely block on that queue after firing off a `u7`.
so what's this thread doing?

it's got two output queues, each with a mutex and condvar: one of cursor location reports (and other future messages to us from the terminal), and one of processed user input (`ncinput`s). we're constantly poll()ing on either one or two descriptors (on Windows, we're using `WaitForMultipleObjects()` on the input handle). whenever there's data available, we read it in a large burst.

all lexing state must be in a self-contained struct carried across reads, so that we can handle mutlibyte sequences (escapes and/or UTF-8) that cross a read boundary.

at each lexing segmentation (end of a UTF8 character or escape), add to the appropriate queue, and signal its condvar.

if an output queue is full, drop its source from the poll. keep looking at any other sources. otherwise, block on a condvar. we'll need a pipe for each queue as well, and we'll want to poll on *that*.

on read, lock the queue. extract the next item if there are any items, and unlock. otherwise, block on a condvar. if the queue was full when you read it, signal the general condvar and write to your pipe.

when we want a cursor report now, we can just freely block on that queue after firing off a `u7`.

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index beced0586b..6ecb62f98e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -496,6 +496,7 @@ foreach(f ${POCSRCS})
   get_filename_component(fe "${f}" NAME_WE)
   add_executable(${fe} ${f})
   target_include_directories(${fe}
+    BEFORE
     PRIVATE include src "${TERMINFO_INCLUDE_DIRS}"
     "${PROJECT_BINARY_DIR}/include"
   )
@@ -511,6 +512,7 @@ foreach(f ${POCPPSRCS})
   get_filename_component(fe "${f}" NAME_WE)
   add_executable(${fe} ${f})
   target_include_directories(${fe}
+    BEFORE
     PRIVATE include src "${TERMINFO_INCLUDE_DIRS}"
     "${PROJECT_BINARY_DIR}/include"
   )
diff --git a/NEWS.md b/NEWS.md
index 75be7d6d63..02a4704333 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,6 +4,14 @@ rearrangements of Notcurses.
 * 2.4.2 (not yet released)
   * You can now set a resize callback on the standard plane.
   * Added `notcurses_getvec()`, providing batched input.
+  * Added `NCOPTION_DRAIN_INPUT`. Notcurses now launches a thread to process
+    input, so that it can respond to terminal messages with minimal latency.
+    Input read from `stdin` intended for the client is buffered until
+    retrieved. If your client never intends to read this input, provide this
+    flag to eliminate unnecessary processing, and ensure Notcurses can always
+    retrieve terminal messages (if buffers are full, Notcurses cannot
+    continue reading). Likewise added `NCDIRECT_OPTION_DRAIN_INPUT`.
+  * Removed a bunch of deprecated `static inline` functions from the headers.
 
 * 2.4.1 (2021-09-12)
   * `notcurses_check_pixel_support()` still returns 0 if there is no support
diff --git a/README.md b/README.md
index 3b5022d4c8..c0c417381a 100644
--- a/README.md
+++ b/README.md
@@ -248,6 +248,25 @@ If things break or seem otherwise lackluster, **please** consult the
   more importantly, it will link against minimal Notcurses installations.
 </details>
 
+<details>
+  <summary>Does it work with hardware terminals?</summary>
+  With the correct `TERM` value, many hardware terminals are supported. The VT100
+  is sadly unsupported due to its extensive need for delays. In general, if the
+  terminfo database entry indicates mandatory delays, Notcurses will not currently
+  support that terminal properly. It's known that Notcurses can drive the VT320
+  and VT340, including Sixel graphics on the latter.
+</details>
+
+<details>
+  <summary>What happens if I try blitting bitmap graphics on a terminal which
+  doesn't support them?</summary>
+  Notcurses will not make use of bitmap protocols unless the terminal positively
+  indicates support for them, even if `NCBLIT_PIXEL` has been requested. Likewise,
+  sextants (`NCBLIT_3x2`) won't be used without Unicode 13 support, etc.
+  `ncvisual_render()` will use the best blitter available, unless
+  `NCVISUAL_OPTION_NODEGRADE` is provided (in which case it will fail).
+</details>
+
 <details>
   <summary>Notcurses looks like absolute crap in <code>screen</code>.</summary>
   <code>screen</code> doesn't support RGB colors (at least as of 4.08.00);
diff --git a/USAGE.md b/USAGE.md
index 8dc2a06112..95e09cbd87 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -95,7 +95,7 @@ typedef enum {
 // to do this, pass NCOPTION_NO_CLEAR_BITMAPS. Note that they might still
 // get cleared even if this is set, and they might not get cleared even if
 // this is not set. It's a tough world out there.
-#define NCOPTION_NO_CLEAR_BITMAPS    0x0002ull
+#define NCOPTION_NO_CLEAR_BITMAPS    0x0002
 
 // We typically install a signal handler for SIGWINCH that generates a resize
 // event in the notcurses_get() queue. Set to inhibit this handler.
@@ -110,7 +110,7 @@ typedef enum {
 // at context creation time. Together with NCOPTION_NO_ALTERNATE_SCREEN and a
 // scrolling standard plane, this facilitates easy scrolling-style programs in
 // rendered mode.
-#define NCOPTION_PRESERVE_CURSOR     0x0010ull
+#define NCOPTION_PRESERVE_CURSOR     0x0010
 
 // Notcurses typically prints version info in notcurses_init() and performance
 // info in notcurses_stop(). This inhibits that output.
@@ -120,6 +120,17 @@ typedef enum {
 // of the "alternate screen". This flag inhibits use of smcup/rmcup.
 #define NCOPTION_NO_ALTERNATE_SCREEN 0x0040
 
+// Do not modify the font. Notcurses might attempt to change the font slightly,
+// to support certain glyphs (especially on the Linux console). If this is set,
+// no such modifications will be made. Note that font changes will not affect
+// anything but the virtual console/terminal in which Notcurses is running.
+#define NCOPTION_NO_FONT_CHANGES     0x0080
+
+// Input may be freely dropped. This ought be provided when the program does not
+// intend to handle input. Otherwise, input can accumulate in internal buffers,
+// eventually preventing Notcurses from processing terminal messages.
+#define NCOPTION_DRAIN_INPUT         0x0100
+
 // Configuration for notcurses_init().
 typedef struct notcurses_options {
   // The name of the terminfo database entry describing this terminal. If NULL,
@@ -378,11 +389,23 @@ struct ncdirect* ncdirect_core_init(const char* termtype, FILE* fp, uint64_t fla
 // echo and line buffering are turned off.
 #define NCDIRECT_OPTION_INHIBIT_CBREAK      0x0002ull
 
+// Input may be freely dropped. This ought be provided when the program does not
+// intend to handle input. Otherwise, input can accumulate in internal buffers,
+// eventually preventing Notcurses from processing terminal messages.
+#define NCDIRECT_OPTION_DRAIN_INPUT         0x0004ull
+
 // We typically install a signal handler for SIG{INT, SEGV, ABRT, QUIT} that
 // restores the screen, and then calls the old signal handler. Set to inhibit
 // registration of these signal handlers. Chosen to match fullscreen mode.
 #define NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS 0x0008ull
 
+// Enable logging (to stderr) at the NCLOGLEVEL_WARNING level.
+#define NCDIRECT_OPTION_VERBOSE             0x0010ull
+
+// Enable logging (to stderr) at the NCLOGLEVEL_TRACE level. This will enable
+// all diagnostics, a superset of NCDIRECT_OPTION_VERBOSE (which this implies).
+#define NCDIRECT_OPTION_VERY_VERBOSE        0x0020ull
+
 // Release 'nc' and any associated resources. 0 on success, non-0 on failure.
 int ncdirect_stop(struct ncdirect* nc);
 ```
@@ -671,7 +694,12 @@ typedef struct ncinput {
 // event is processed, the return value is the 'id' field from that event.
 // 'ni' may be NULL.
 uint32_t notcurses_get(struct notcurses* n, const struct timespec* ts,
-                       ncinput* ni)
+                       ncinput* ni);
+
+// Acquire up to 'vcount' ncinputs at the vector 'ni'. The number read will be
+// returned, or -1 on error without any reads, 0 on timeout.
+int notcurses_getvec(struct notcurses* n, const struct timespec* ts,
+                     ncinput* ni, int vcount);
 
 // 'ni' may be NULL if the caller is uninterested in event details. If no event
 // is ready, returns 0.
diff --git a/doc/man/man3/notcurses_direct.3.md b/doc/man/man3/notcurses_direct.3.md
index 9ba19f7882..05d06f9282 100644
--- a/doc/man/man3/notcurses_direct.3.md
+++ b/doc/man/man3/notcurses_direct.3.md
@@ -169,6 +169,11 @@ The following flags are defined:
     will place the terminal into cbreak mode (i.e. disabling echo and line
     buffering; see **tcgetattr(3)**).
 
+* **NCDIRECT_OPTION_DRAIN_INPUT**: Standard input may be freely discarded. If
+    you do not intend to process input, pass this flag. Otherwise, input can
+    buffer up, eventually preventing Notcurses from processing terminal
+    messages. It will furthermore avoid wasting time processing useless input.
+
 * **NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS**: A signal handler will usually be installed
     for **SIGABRT**, **SIGFPE**, **SIGILL**, **SIGINT**, **SIGQUIT**,
     **SIGSEGV**, and **SIGTERM**, cleaning up the terminal on such exceptions.
diff --git a/doc/man/man3/notcurses_init.3.md b/doc/man/man3/notcurses_init.3.md
index 20f43119a0..bfa1be98f2 100644
--- a/doc/man/man3/notcurses_init.3.md
+++ b/doc/man/man3/notcurses_init.3.md
@@ -19,6 +19,7 @@ notcurses_init - initialize a notcurses instance
 #define NCOPTION_SUPPRESS_BANNERS    0x0020ull
 #define NCOPTION_NO_ALTERNATE_SCREEN 0x0040ull
 #define NCOPTION_NO_FONT_CHANGES     0x0080ull
+#define NCOPTION_DRAIN_INPUT         0x0100ull
 
 typedef enum {
   NCLOGLEVEL_SILENT,  // print nothing once fullscreen service begins
@@ -139,6 +140,11 @@ zero. The following flags are defined:
 * **NCOPTION_NO_FONT_CHANGES**: Do not touch the font. Notcurses might
     otherwise attempt to extend the font, especially in the Linux console.
 
+* **NCOPTION_DRAIN_INPUT**: Standard input may be freely discarded. If you do not
+    intend to process input, pass this flag. Otherwise, input can buffer up, and
+    eventually prevent Notcurses from processing messages from the terminal. It
+    will furthermore avoid wasting time processing useless input.
+
 ## Fatal signals
 
 It is important to reset the terminal before exiting, whether terminating due
diff --git a/doc/man/man3/notcurses_input.3.md b/doc/man/man3/notcurses_input.3.md
index 096f68cf33..99b5ad7ea3 100644
--- a/doc/man/man3/notcurses_input.3.md
+++ b/doc/man/man3/notcurses_input.3.md
@@ -31,6 +31,8 @@ typedef struct ncinput {
 
 **uint32_t notcurses_get(struct notcurses* ***n***, const struct timespec* ***ts***, ncinput* ***ni***);**
 
+**int notcurses_getvec(struct notcurses* ***n***, const struct timespec* ***ts***, ncinput* ***ni***, int vcount);**
+
 **uint32_t notcurses_getc_nblock(struct notcurses* ***n***, ncinput* ***ni***);**
 
 **uint32_t notcurses_getc_blocking(struct notcurses* ***n***, ncinput* ***ni***);**
@@ -65,9 +67,8 @@ to fill whenever it reads.
 **notcurses_get** allows a **struct timespec** to be specified as a timeout.
 If **ts** is **NULL**, **notcurses_get** will block until it reads input, or
 is interrupted by a signal. If its values are zeroes, there will be no blocking.
-Otherwise, **ts** specifies a minimum time to wait for input before giving up.
-On timeout, 0 is returned. Signals in **sigmask** will be masked and blocked in
-the same manner as a call to **ppoll(2)**. **sigmask** may be **NULL**. Event
+Otherwise, **ts** specifies an absolute deadline (using the same source and
+timezone as **gettimeofday(2)**). On timeout, 0 is returned. Event
 details will be reported in **ni**, unless **ni** is NULL.
 
 **notcurses_inputready_fd** provides a file descriptor suitable for use with
@@ -128,6 +129,10 @@ temporary one (especially e.g. **EINTR**), **notcurses_get** probably cannot
 be usefully called forthwith. On a timeout, 0 is returned. Otherwise, the
 UCS-32 value of a Unicode codepoint, or a synthesized event, is returned.
 
+If an error is encountered before **notcurses_getvec** has read any input,
+it will return -1. If it times out before reading any input, it will return
+0. Otherwise, it returns the number of **ncinput** objects written back.
+
 **notcurses_mouse_enable** returns 0 on success, and non-zero on failure, as
 does **notcurses_mouse_disable**.
 
@@ -175,6 +180,7 @@ are resolved.
 
 # SEE ALSO
 
+**gettimeofday(2)**,
 **poll(2)**,
 **notcurses(3)**,
 **notcurses_refresh(3)**,
diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h
index 86651d39cb..00bd10d30c 100644
--- a/include/notcurses/direct.h
+++ b/include/notcurses/direct.h
@@ -24,6 +24,11 @@ extern "C" {
 // echo and input's line buffering are turned off.
 #define NCDIRECT_OPTION_INHIBIT_CBREAK      0x0002ull
 
+// Input may be freely dropped. This ought be provided when the program does not
+// intend to handle input. Otherwise, input can accumulate in internal buffers,
+// eventually preventing Notcurses from processing terminal messages.
+#define NCDIRECT_OPTION_DRAIN_INPUT         0x0004ull
+
 // We typically install a signal handler for SIG{INT, SEGV, ABRT, QUIT} that
 // restores the screen, and then calls the old signal handler. Set to inhibit
 // registration of these signal handlers. Chosen to match fullscreen mode.
@@ -489,11 +494,6 @@ ncdirect_canbraille(const struct ncdirect* nc){
 API bool ncdirect_canget_cursor(const struct ncdirect* nc)
   __attribute__ ((nonnull (1)));
 
-// Deprecated, to be removed for ABI3. Use ncdirect_get() in new code.
-API uint32_t ncdirect_getc(struct ncdirect* n, const struct timespec* ts,
-                           const void* unused, ncinput* ni)
-  __attribute__ ((deprecated)) __attribute__ ((nonnull (1)));
-
 #undef ALLOC
 #undef API
 
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index f49dd61d3c..bf3f0c7f33 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -48,6 +48,7 @@ struct nctablet;  // grouped item within an ncreel
 struct ncreel;    // hierarchical block-based data browser
 struct nctab;     // grouped item within an nctabbed
 struct nctabbed;  // widget with one tab visible at a time
+struct ncdirect;  // direct mode context
 
 // we never blit full blocks, but instead spaces (more efficient) with the
 // background set to the desired foreground. these need be kept in the same
@@ -900,6 +901,11 @@ typedef enum {
 // anything but the virtual console/terminal in which Notcurses is running.
 #define NCOPTION_NO_FONT_CHANGES     0x0080ull
 
+// Input may be freely dropped. This ought be provided when the program does not
+// intend to handle input. Otherwise, input can accumulate in internal buffers,
+// eventually preventing Notcurses from processing terminal messages.
+#define NCOPTION_DRAIN_INPUT         0x0100ull
+
 // "CLI mode" is just NCOPTION_NO_CLEAR_BITMAPS | NCOPTION_NO_ALTERNATE_SCREEN |
 // NCOPTION_PRESERVE_CURSOR, plus enabling scrolling on the standard plane.
 
@@ -1064,11 +1070,18 @@ ncinput_equal_p(const ncinput* n1, const ncinput* n2){
 // timespec to bound blocking. Returns a single Unicode code point, or
 // (uint32_t)-1 on error. 'sigmask' may be NULL. Returns 0 on a timeout. If an
 // event is processed, the return value is the 'id' field from that event.
-// 'ni' may be NULL.
+// 'ni' may be NULL. 'ts' is an *absolute* time relative to gettimeofday()
+// (see pthread_cond_timedwait(3)).
 API uint32_t notcurses_get(struct notcurses* n, const struct timespec* ts,
                            ncinput* ni)
   __attribute__ ((nonnull (1)));
 
+// Acquire up to 'vcount' ncinputs at the vector 'ni'. The number read will be
+// returned, or -1 on error without any reads, 0 on timeout.
+API int notcurses_getvec(struct notcurses* n, const struct timespec* ts,
+                         ncinput* ni, int vcount)
+  __attribute__ ((nonnull (1, 3)));
+
 // Get a file descriptor suitable for input event poll()ing. When this
 // descriptor becomes available, you can call notcurses_getc_nblock(),
 // and input ought be ready. This file descriptor is *not* necessarily
@@ -4019,17 +4032,19 @@ API int notcurses_render_to_file(struct notcurses* nc, FILE* fp)
 API void notcurses_debug_caps(const struct notcurses* nc, FILE* debugfp)
   __attribute__ ((deprecated)) __attribute__ ((nonnull (1, 2)));
 
-// Backwards-compatibility wrapper; this will be removed for ABI3.
-// Use notcurses_get() in new code.
-API uint32_t notcurses_getc(struct notcurses* n, const struct timespec* ts,
-                            const void* unused, ncinput* ni)
-  __attribute__ ((deprecated)) __attribute__ ((nonnull (1)));
-
 __attribute__ ((deprecated)) API int nccell_width(const struct ncplane* n, const nccell* c);
 
 API ALLOC char* ncvisual_subtitle(const struct ncvisual* ncv)
   __attribute__ ((nonnull (1))) __attribute__ ((deprecated));
 
+API uint32_t notcurses_getc(struct notcurses* nc, const struct timespec* ts,
+                            const void* unused, ncinput* ni)
+  __attribute__ ((nonnull (1))) __attribute__ ((deprecated));
+
+API uint32_t ncdirect_getc(struct ncdirect* nc, const struct timespec *ts,
+                           const void* unused, ncinput* ni)
+  __attribute__ ((nonnull (1))) __attribute__ ((deprecated));
+
 #undef ALLOC
 #undef API
 
diff --git a/src/demo/whiteout.c b/src/demo/whiteout.c
index bf2577bcae..a2a5c8d481 100644
--- a/src/demo/whiteout.c
+++ b/src/demo/whiteout.c
@@ -210,7 +210,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total,
   ncplane_printf_yx(n, 1, 4, " %03dx%03d (%d/%d) ", maxx, maxy, num + 1, total);
   ncplane_off_styles(n, NCSTYLE_ITALIC);
   ncplane_set_fg_rgb8(n, 224, 128, 224);
-  ncplane_putstr_yx(n, 3, 1, " 🎆🔥 unicode 13, resize awareness, 24b truecolor…🔥🎆 ");
+  ncplane_putstr_yx(n, 3, 1, " 🎆🔥 unicode 14, resize awareness, 24b truecolor…🔥🎆 ");
   ncplane_set_fg_rgb8(n, 255, 255, 255);
   return 0;
 }
diff --git a/src/fetch/main.c b/src/fetch/main.c
index 0fa92cfacc..95ad74d6cf 100644
--- a/src/fetch/main.c
+++ b/src/fetch/main.c
@@ -664,7 +664,9 @@ ncneofetch(struct notcurses* nc){
   fi.hostname = notcurses_hostname();
   fi.username = notcurses_accountname();
   fetch_env_vars(nc, &fi);
-  fetch_x_props(&fi);
+  if(kern != NCNEO_XNU && kern != NCNEO_WINDOWS){
+    fetch_x_props(&fi);
+  }
   if(kern == NCNEO_LINUX){
     fetch_cpu_info(&fi);
   }else{
@@ -695,7 +697,8 @@ int main(int argc, char** argv){
     .flags = NCOPTION_SUPPRESS_BANNERS
              | NCOPTION_NO_ALTERNATE_SCREEN
              | NCOPTION_NO_CLEAR_BITMAPS
-             | NCOPTION_PRESERVE_CURSOR,
+             | NCOPTION_PRESERVE_CURSOR
+             | NCOPTION_DRAIN_INPUT,
   };
   if(argc > 2){
     usage(argv[0], stderr);
diff --git a/src/info/main.c b/src/info/main.c
index e8f8e84d7b..60327b0e0e 100644
--- a/src/info/main.c
+++ b/src/info/main.c
@@ -429,7 +429,8 @@ int main(int argc, const char** argv){
   notcurses_options nopts = {
     .flags = NCOPTION_NO_ALTERNATE_SCREEN
              | NCOPTION_PRESERVE_CURSOR
-             | NCOPTION_NO_CLEAR_BITMAPS,
+             | NCOPTION_NO_CLEAR_BITMAPS
+             | NCOPTION_DRAIN_INPUT,
   };
   if(argc > 2){
     usage(*argv, stderr, EXIT_FAILURE);
diff --git a/src/input/input.cpp b/src/input/input.cpp
index 934c6002e7..cf9ee886e1 100644
--- a/src/input/input.cpp
+++ b/src/input/input.cpp
@@ -218,13 +218,13 @@ int input_demo(ncpp::NotCurses* nc) {
   }
   struct ncplane_options nopts = {
     .y = dimy - PLOTHEIGHT - 1,
-    .x = 0,
+    .x = NCALIGN_CENTER,
     .rows = PLOTHEIGHT,
-    .cols = cols,
+    .cols = cols / 2,
     .userptr = nullptr,
     .name = "plot",
     .resizecb = nullptr, // FIXME
-    .flags = 0,
+    .flags = NCPLANE_OPTION_HORALIGNED,
     .margin_b = 0,
     .margin_r = 0,
   };
@@ -356,6 +356,10 @@ int main(int argc, char** argv){
     return EXIT_FAILURE;
   }
   notcurses_options nopts{};
+  nopts.margin_t = 2;
+  nopts.margin_l = 2;
+  nopts.margin_r = 2;
+  nopts.margin_b = 2;
   nopts.loglevel = NCLOGLEVEL_ERROR;
   if(argc > 2){
     usage(argv[0], stderr);
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 9a6dc5f34c..19b8b84706 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -179,77 +179,16 @@ int ncdirect_cursor_disable(ncdirect* nc){
 }
 
 static int
-cursor_yx_get(int ttyfd, const char* u7, int* y, int* x){
-  if(tty_emit(u7, ttyfd)){
+cursor_yx_get(ncdirect* n, int ttyfd, const char* u7, int* y, int* x){
+  struct inputctx* ictx = n->tcache.ictx;
+  if(ncdirect_flush(n)){
     return -1;
   }
-  bool done = false;
-  enum { // what we expect now
-    CURSOR_ESC, // 27 (0x1b)
-    CURSOR_LSQUARE,
-    CURSOR_ROW, // delimited by a semicolon
-    CURSOR_COLUMN,
-    CURSOR_R,
-  } state = CURSOR_ESC;
-  int row = 0, column = 0;
-  int r;
-  char in;
-  do{
-#ifndef __MINGW64__
-    while((r = read(ttyfd, &in, 1)) == 1){
-#else
-    while((r = getc(stdin)) >= 0){ // FIXME
-      in = r;
-#endif
-      bool valid = false;
-      switch(state){
-        case CURSOR_ESC: valid = (in == NCKEY_ESC); state = CURSOR_LSQUARE; break;
-        case CURSOR_LSQUARE: valid = (in == '['); state = CURSOR_ROW; break;
-        case CURSOR_ROW:
-          if(isdigit(in)){
-            row *= 10;
-            row += in - '0';
-            valid = true;
-          }else if(in == ';'){
-            state = CURSOR_COLUMN;
-            valid = true;
-          }
-          break;
-        case CURSOR_COLUMN:
-          if(isdigit(in)){
-            column *= 10;
-            column += in - '0';
-            valid = true;
-          }else if(in == 'R'){
-            state = CURSOR_R;
-            valid = true;
-          }
-          break;
-        case CURSOR_R: default: // logical error, whoops
-          break;
-      }
-      if(!valid){
-        logerror("Unexpected result (%c, %d) from terminal\n", in, in);
-        break;
-      }
-      if(state == CURSOR_R){
-        done = true;
-        break;
-      }
-    }
-    // need to loop 0 to handle slow terminals, see for instance screen =[
-  }while(!done && (r >= 0 || (errno == EINTR || errno == EAGAIN || errno == EBUSY)));
-  if(!done){
-    logerror("Error reading cursor location\n");
+  if(tty_emit(u7, ttyfd)){
     return -1;
   }
-  if(y){
-    *y = row;
-  }
-  if(x){
-    *x = column;
-  }
-  loginfo("cursor at y=%d x=%d\n", row, column);
+  get_cursor_location(ictx, y, x);
+  loginfo("cursor at y=%d x=%d\n", *y, *x);
   return 0;
 }
 
@@ -266,7 +205,7 @@ int ncdirect_cursor_move_yx(ncdirect* n, int y, int x){
     if(hpa){
       return term_emit(tiparm(hpa, x), n->ttyfp, false);
     }else if(n->tcache.ttyfd >= 0 && u7){
-      if(cursor_yx_get(n->tcache.ttyfd, u7, &y, NULL)){
+      if(cursor_yx_get(n, n->tcache.ttyfd, u7, &y, NULL)){
         return -1;
       }
     }else{
@@ -276,7 +215,7 @@ int ncdirect_cursor_move_yx(ncdirect* n, int y, int x){
     if(!vpa){
       return term_emit(tiparm(vpa, y), n->ttyfp, false);
     }else if(n->tcache.ttyfd >= 0 && u7){
-      if(cursor_yx_get(n->tcache.ttyfd, u7, NULL, &x)){
+      if(cursor_yx_get(n, n->tcache.ttyfd, u7, NULL, &x)){
         return -1;
       }
     }else{
@@ -295,6 +234,7 @@ int ncdirect_cursor_move_yx(ncdirect* n, int y, int x){
   return -1; // we will not be moving the cursor today
 }
 
+/*
 // an algorithm to detect inverted cursor reporting on terminals 2x2 or larger:
 //  * get initial cursor position / push cursor position
 //  * move right using cursor-independent routines
@@ -408,6 +348,7 @@ detect_cursor_inversion_wrapper(ncdirect* n, const char* u7, int* y, int* x){
   // of ttyfd, as needed by cursor interrogation.
   return detect_cursor_inversion(n, u7, toty, totx, y, x);
 }
+*/
 
 // no terminfo capability for this. dangerous--it involves writing controls to
 // the terminal, and then reading a response. many things can distupt this
@@ -444,24 +385,7 @@ int ncdirect_cursor_yx(ncdirect* n, int* y, int* x){
   if(!x){
     x = &xval;
   }
-  if(!n->tcache.detected_cursor_inversion){
-    ret = detect_cursor_inversion_wrapper(n, u7, y, x);
-  }else{
-    ret = cursor_yx_get(n->tcache.ttyfd, u7, y, x);
-  }
-  if(ret == 0){
-    if(n->tcache.inverted_cursor){
-      int tmp = *y;
-      *y = *x;
-      *x = tmp;
-    }else{
-      // we use 0-based coordinates, but known terminals use 1-based
-      // coordinates. the only known exception is kmscon, which is
-      // incidentally the only one which inverts its response.
-      --*y;
-      --*x;
-    }
-  }
+  ret = cursor_yx_get(n, n->tcache.ttyfd, u7, y, x);
   if(tcsetattr(n->tcache.ttyfd, TCSANOW, &oldtermios)){
     fprintf(stderr, "Couldn't restore terminal mode on %d (%s)\n",
             n->tcache.ttyfd, strerror(errno)); // don't return error for this
@@ -510,7 +434,7 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
   if(np->sprite){
     int y;
     const char* u7 = get_escape(&n->tcache, ESCAPE_U7);
-    if(cursor_yx_get(n->tcache.ttyfd, u7, &y, NULL)){
+    if(cursor_yx_get(n, n->tcache.ttyfd, u7, &y, NULL)){
       return -1;
     }
     if(ncdirect_cursor_move_yx(n, y, xoff)){
@@ -522,10 +446,10 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
       return -1;
     }
     fbuf f = {};
-    if(fbuf_init(&f)){
+    if(cursor_yx_get(n, n->tcache.ttyfd, u7, &y, NULL)){
       return -1;
     }
-    if(cursor_yx_get(n->tcache.ttyfd, u7, &y, NULL)){
+    if(fbuf_init(&f)){
       return -1;
     }
     if(toty - dimy < y){
@@ -538,21 +462,16 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
       // perform our scrolling outside of the fbuf framework, as we need it
       // to happen immediately for fbdcon
       if(ncdirect_cursor_move_yx(n, y, xoff)){
+        fbuf_free(&f);
         return -1;
       }
-      const char* indn = get_escape(&n->tcache, ESCAPE_IND);
-      if(scrolls > 1 && indn){
-        if(term_emit(tiparm(indn, scrolls), stdout, true) < 0){
-          return -1;
-        }
-      }
-      while(scrolls--){
-        if(ncfputc('\v', stdout) < 0){
-          return -1;
-        }
+      if(emit_scrolls(&n->tcache, scrolls, &f) < 0){
+        fbuf_free(&f);
+        return -1;
       }
     }
     if(sprite_draw(&n->tcache, NULL, np->sprite, &f, y, xoff) < 0){
+      fbuf_free(&f);
       return -1;
     }
     if(sprite_commit(&n->tcache, &f, np->sprite, true)){
@@ -864,18 +783,14 @@ ncdirect_stop_minimal(void* vnc){
     ret |= fbuf_finalize(&f, stdout);
   }
   if(nc->tcache.ttyfd >= 0){
-    if(nc->tcache.kittykbd){
-      ret |= tty_emit("\x1b[<u", nc->tcache.ttyfd);
-    }
+    ret |= tty_emit("\x1b[<u", nc->tcache.ttyfd);
     const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM);
     if(cnorm && tty_emit(cnorm, nc->tcache.ttyfd)){
       ret = -1;
     }
     ret |= tcsetattr(nc->tcache.ttyfd, TCSANOW, nc->tcache.tpreserved);
-    ret |= close(nc->tcache.ttyfd);
   }
   ret |= ncdirect_flush(nc);
-  free_terminfo_cache(&nc->tcache);
   return ret;
 }
 
@@ -883,7 +798,7 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
   if(outfp == NULL){
     outfp = stdout;
   }
-  if(flags > (NCDIRECT_OPTION_VERY_VERBOSE << 1)){ // allow them through with warning
+  if(flags > (NCDIRECT_OPTION_DRAIN_INPUT << 1)){ // allow them through with warning
     logwarn("Passed unsupported flags 0x%016jx\n", (uintmax_t)flags);
   }
   ncdirect* ret = malloc(sizeof(ncdirect));
@@ -891,6 +806,10 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
     return ret;
   }
   memset(ret, 0, sizeof(*ret));
+  if(pthread_mutex_init(&ret->stats.lock, NULL)){
+    free(ret);
+    return NULL;
+  }
   ret->flags = flags;
   ret->ttyfp = outfp;
   if(!(flags & NCDIRECT_OPTION_INHIBIT_SETLOCALE)){
@@ -903,6 +822,7 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
   }
   if(setup_signals(ret, (flags & NCDIRECT_OPTION_NO_QUIT_SIGHANDLERS),
                    true, ncdirect_stop_minimal)){
+    pthread_mutex_destroy(&ret->stats.lock);
     free(ret);
     return NULL;
   }
@@ -917,9 +837,10 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
   }
   int cursor_y = -1;
   int cursor_x = -1;
-  if(interrogate_terminfo(&ret->tcache, termtype, ret->ttyfp, utf8,
-                          1, flags & NCDIRECT_OPTION_INHIBIT_CBREAK,
-                          TERMINAL_UNKNOWN, &cursor_y, &cursor_x, NULL)){
+  if(interrogate_terminfo(&ret->tcache, termtype, ret->ttyfp, utf8, 1,
+                          flags & NCDIRECT_OPTION_INHIBIT_CBREAK,
+                          0, &cursor_y, &cursor_x, &ret->stats, 0, 0,
+                          flags & NCDIRECT_OPTION_DRAIN_INPUT)){
     goto err;
   }
   if(cursor_y >= 0){
@@ -927,6 +848,7 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
     // unaffected by any query spill (unconsumed control sequences). move
     // us back to that location, in case there was any such spillage.
     if(ncdirect_cursor_move_yx(ret, cursor_y, cursor_x)){
+      free_terminfo_cache(&ret->tcache);
       goto err;
     }
   }
@@ -942,6 +864,7 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
     (void)tcsetattr(ret->tcache.ttyfd, TCSANOW, ret->tcache.tpreserved);
   }
   drop_signals(ret);
+  pthread_mutex_destroy(&ret->stats.lock);
   free(ret);
   return NULL;
 }
@@ -950,6 +873,11 @@ int ncdirect_stop(ncdirect* nc){
   int ret = 0;
   if(nc){
     ret |= ncdirect_stop_minimal(nc);
+    free_terminfo_cache(&nc->tcache);
+    if(nc->tcache.ttyfd >= 0){
+      ret |= close(nc->tcache.ttyfd);
+    }
+    pthread_mutex_destroy(&nc->stats.lock);
     free(nc);
   }
   return ret;
diff --git a/src/lib/fd.c b/src/lib/fd.c
index ecb554c4e5..79faaf97c4 100644
--- a/src/lib/fd.c
+++ b/src/lib/fd.c
@@ -135,8 +135,7 @@ int ncfdplane_destroy(ncfdplane* n){
       n->destroyed = true; // ncfdplane_destroy_inner() is called on thread exit
     }else{
       void* vret = NULL;
-      pthread_cancel(n->tid);
-      ret |= pthread_join(n->tid, &vret);
+      ret |= cancel_and_join("fdplane", n->tid, &vret);
       ret |= ncfdplane_destroy_inner(n);
     }
   }
diff --git a/src/lib/gpm.c b/src/lib/gpm.c
index dc0baa6b4d..40f3fafc35 100644
--- a/src/lib/gpm.c
+++ b/src/lib/gpm.c
@@ -32,7 +32,7 @@ gpmwatcher(void* vti){
       logwarn("input overflowed %hd %hd\n", gev.x, gev.y);
       continue;
     }
-    ncinput_shovel(&ti->input, cmdbuf, strlen(cmdbuf));
+    ncinput_shovel(ti->ictx, cmdbuf, strlen(cmdbuf));
   }
   return NULL;
 }
@@ -66,13 +66,8 @@ int gpm_read(tinfo* ti, ncinput* ni){
 }
 
 int gpm_close(tinfo* ti){
-  if(pthread_cancel(ti->gpmthread)){
-    logerror("couldn't cancel gpm thread\n"); // daemon might have died
-  }
   void* thrres;
-  if(pthread_join(ti->gpmthread, &thrres)){
-    logerror("error joining gpm thread\n");
-  }
+  cancel_and_join("gpm", ti->gpmthread, &thrres);
   Gpm_Close();
   memset(&gpmconn, 0, sizeof(gpmconn));
   return 0;
diff --git a/src/lib/in.c b/src/lib/in.c
new file mode 100644
index 0000000000..9ecfc74bfc
--- /dev/null
+++ b/src/lib/in.c
@@ -0,0 +1,2007 @@
+#include <stdio.h>
+#include "internal.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 (NCOPTION_TOSS_INPUT)
+
+// FIXME still need to:
+//  integrate main specials trie with automaton, enable input_errors
+//  probably want pipes/eventfds rather than SIGCONT
+
+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;
+  }
+}
+
+// data collected from responses to our terminal queries.
+typedef struct termqueries {
+  int celly, cellx;     // cell geometry on startup
+  int pixy, pixx;       // pixel geometry on startup
+  int cursory, cursorx; // cursor location on startup
+  unsigned kittygraphs; // are kitty graphics supported?
+  int sixely, sixelx;   // maximum sixel size
+  int cregs;            // sixel color registers
+  unsigned appsync;     // application-sync supported?
+} termqueries;
+
+typedef struct cursorloc {
+  int y, x;             // 0-indexed cursor location
+} cursorloc;
+
+typedef enum {
+  STATE_NULL,
+  STATE_ESC,  // escape; aborts any active sequence
+  STATE_CSI,  // control sequence introducer
+  STATE_DCS,  // device control string
+  // XTVERSION replies with DCS > | ... ST
+  STATE_XTVERSION1,
+  STATE_XTVERSION2,
+  // XTGETTCAP replies with DCS 1 + r for a good request, or 0 + r for bad
+  STATE_XTGETTCAP1, // XTGETTCAP, got '0/1' (DCS 0/1 + r Pt ST)
+  STATE_XTGETTCAP2, // XTGETTCAP, got '+' (DCS 0/1 + r Pt ST)
+  STATE_XTGETTCAP3, // XTGETTCAP, got 'r' (DCS 0/1 + r Pt ST)
+  STATE_XTGETTCAP_TERMNAME1, // got property 544E, 'TN' (terminal name) first hex nibble
+  STATE_XTGETTCAP_TERMNAME2, // got property 544E, 'TN' (terminal name) second hex nibble
+  STATE_DCS_DRAIN,  // throw away input until we hit escape
+  STATE_APC,        // application programming command, starts with \x1b_
+  STATE_APC_DRAIN,  // looking for \x1b
+  STATE_APC_ST,     // looking for ST
+  STATE_BG1,        // got '1'
+  STATE_BG2,        // got second '1'
+  STATE_BGSEMI,     // got '11;', draining string to ESC ST
+  STATE_TDA1, // tertiary DA, got '!'
+  STATE_TDA2, // tertiary DA, got '|', first hex nibble
+  STATE_TDA3, // tertiary DA, second hex nibble
+  STATE_SDA,  // secondary DA (CSI > Pp ; Pv ; Pc c)
+  STATE_SDA_VER,  // secondary DA, got semi, reading to next semi
+  STATE_SDA_DRAIN, // drain secondary DA to 'c'
+  STATE_DA,   // primary DA   (CSI ? ... c) OR XTSMGRAPHICS OR DECRPM or kittykbd
+  STATE_DA_DRAIN, // drain out the primary DA to an alpha
+  STATE_DA_SEMI,    // got first semicolon following numeric
+  STATE_DA_SEMI2,   // got second semicolon following numeric ; numeric
+  STATE_DA_SEMI3,   // got third semicolon following numeric ; numeric ; numeric
+  STATE_APPSYNC_REPORT, // got DECRPT ?2026
+  STATE_APPSYNC_REPORT_DRAIN, // drain out decrpt to 'y'
+  // cursor location report: CSI row ; col R
+  // text area pixel geometry: CSI 4 ; rows ; cols t
+  // text area cell geometry: CSI 8 ; rows ; cols t
+  // so we handle them the same until we hit either a second semicolon or an
+  // 'R', 't', or 'u'. at the second ';', we verify that the first variable was
+  // '4' or '8', and continue to 't' via STATE_{PIXELS,CELLS}_WIDTH.
+  STATE_CURSOR_COL,    // reading numeric to 'R', 't', 'u', or ';'
+  STATE_PIXELS_WIDTH,  // reading text area width in pixels to ';'
+  STATE_CELLS_WIDTH,   // reading text area width in cells to ';'
+  STATE_MOUSE,         // got '<', looking for mouse coordinates
+  STATE_MOUSE2,        // got mouse click modifiers
+  STATE_MOUSE3,        // got first mouse coordinate
+} initstates_e;
+
+// we assumed escapes can only be composed of 7-bit chars
+typedef struct esctrie {
+  struct esctrie** trie;  // if non-NULL, next level of radix-128 trie
+  uint32_t special;       // composed key terminating here
+  bool shift, ctrl, alt;
+} esctrie;
+
+static esctrie*
+create_esctrie_node(int special){
+  esctrie* e = malloc(sizeof(*e));
+  if(e){
+    e->trie = NULL;
+    e->special = special;
+    e->shift = 0;
+    e->ctrl = 0;
+    e->alt = 0;
+  }
+  return e;
+}
+
+// local state for the input thread. don't put this large struct on the stack.
+typedef struct inputctx {
+  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 __MINGW64__
+  HANDLE stdinhandle;   // handle to input terminal for MSFT Terminal
+#endif
+
+  uint64_t seqnum;      // process-scope sequence number
+  int lmargin, tmargin; // margins in use at left and top
+
+  struct esctrie* inputescapes;
+
+  // 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 ibuf[BUFSIZ]; // might be intermingled bulk/control data
+  unsigned char tbuf[BUFSIZ]; // only used if we have distinct terminal fd
+  int ibufvalid;      // we mustn't read() if ibufvalid == sizeof(ibuf)
+  int tbufvalid;      // only used if we have distinct terminal connection
+
+  // transient state for processing control sequences
+  // stringstate is the state at which this string was initialized, and can be
+  // one of STATE_XTVERSION1, STATE_XTGETTCAP_TERMNAME1, STATE_TDA1, and STATE_BG1
+  initstates_e state, stringstate;
+  int numeric;            // currently-lexed numeric
+  char runstring[BUFSIZ]; // running string (when stringstate != STATE_NULL)
+  int stridx;             // length of runstring
+  int p2, p3, p4;         // holders for numeric params
+  struct esctrie* triepos;// position in escapes automaton
+
+  // ringbuffers for processed, structured input
+  cursorloc* csrs;    // cursor reports are dumped here
+  ncinput* inputs;    // processed input is dumped here
+  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 linesigs;  // are line discipline signals active?
+  unsigned drain;     // drain away bulk input?
+  ncsharedstats *stats; // stats shared with notcurses context
+
+  struct initial_responses* initdata;
+  struct initial_responses* initdata_complete;
+} inputctx;
+
+static void
+input_free_esctrie(esctrie** eptr){
+  esctrie* e;
+  if( (e = *eptr) ){
+    if(e->trie){
+      int z;
+      for(z = 0 ; z < 0x80 ; ++z){
+        if(e->trie[z]){
+          input_free_esctrie(&e->trie[z]);
+        }
+      }
+      free(e->trie);
+    }
+    free(e);
+  }
+}
+
+// multiple input escapes might map to the same input
+static int
+inputctx_add_input_escape(inputctx* ictx, const char* esc, uint32_t special,
+                          unsigned shift, unsigned ctrl, unsigned alt){
+  if(esc[0] != NCKEY_ESC || strlen(esc) < 2){ // assume ESC prefix + content
+    logerror("not an escape (0x%x)\n", special);
+    return -1;
+  }
+  if(ictx->inputescapes == NULL){
+    if((ictx->inputescapes = create_esctrie_node(NCKEY_INVALID)) == NULL){
+      return -1;
+    }
+  }
+  esctrie* cur = ictx->inputescapes;
+  ++esc; // don't encode initial escape as a transition
+  do{
+    int valid = *esc;
+    if(valid <= 0 || valid >= 0x80 || valid == NCKEY_ESC){
+      logerror("invalid character %d in escape\n", valid);
+      return -1;
+    }
+    if(cur->trie == NULL){
+      const size_t tsize = sizeof(cur->trie) * 0x80;
+      if((cur->trie = malloc(tsize)) == NULL){
+        return -1;
+      }
+      memset(cur->trie, 0, tsize);
+    }
+    if(cur->trie[valid] == NULL){
+      if((cur->trie[valid] = create_esctrie_node(NCKEY_INVALID)) == NULL){
+        return -1;
+      }
+    }
+    cur = cur->trie[valid];
+    ++esc;
+  }while(*esc);
+  // it appears that multiple keys can be mapped to the same escape string. as
+  // an example, see "kend" and "kc1" in st ("simple term" from suckless) :/.
+  if(cur->special != NCKEY_INVALID){ // already had one here!
+    if(cur->special != special){
+      logwarn("already added escape (got 0x%x, wanted 0x%x)\n", cur->special, special);
+    }
+  }else{
+    cur->special = special;
+    cur->shift = shift;
+    cur->ctrl = ctrl;
+    cur->alt = alt;
+  }
+  return 0;
+}
+
+// load all known special keys from terminfo, and build the input sequence trie
+static int
+prep_special_keys(inputctx* ictx){
+#ifndef __MINGW64__
+  static const struct {
+    const char* tinfo;
+    uint32_t key;
+    bool shift, ctrl, alt;
+  } keys[] = {
+    { .tinfo = "kcbt",  .key = '\t', .shift = true, },
+    { .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 = "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 = NCKEY_INVALID, }
+  }, *k;
+  for(k = keys ; k->tinfo ; ++k){
+    char* seq = tigetstr(k->tinfo);
+    if(seq == NULL || seq == (char*)-1){
+      loginfo("no terminfo declaration for %s\n", k->tinfo);
+      continue;
+    }
+    if(seq[0] != NCKEY_ESC || strlen(seq) < 2){ // assume ESC prefix + content
+      logwarn("invalid escape: %s (0x%x)\n", k->tinfo, k->key);
+      continue;
+    }
+    if(inputctx_add_input_escape(ictx, seq, k->key, k->shift, k->ctrl, k->alt)){
+      return -1;
+    }
+    logdebug("support for terminfo's %s: %s\n", k->tinfo, seq);
+  }
+#endif
+  return 0;
+}
+
+static inline inputctx*
+create_inputctx(tinfo* ti, FILE* infp, int lmargin, int tmargin,
+                ncsharedstats* stats, unsigned drain){
+  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_cond_init(&i->icond, NULL) == 0){
+            if(pthread_mutex_init(&i->clock, NULL) == 0){
+              if(pthread_cond_init(&i->ccond, NULL) == 0){
+                if((i->stdinfd = fileno(infp)) >= 0){
+                  if( (i->initdata = malloc(sizeof(*i->initdata))) ){
+                    i->inputescapes = NULL;
+                    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));
+                        i->state = i->stringstate = STATE_NULL;
+                        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->ibufvalid = 0;
+                        // FIXME need to get this out of the initial termios
+                        // (as stored in tpreserved)
+                        i->linesigs = 1;
+                        i->tbufvalid = 0;
+                        i->numeric = 0;
+                        i->stridx = 0;
+                        i->runstring[i->stridx] = '\0';
+                        i->lmargin = lmargin;
+                        i->tmargin = tmargin;
+                        i->seqnum = 0;
+                        i->drain = drain;
+                        logdebug("input descriptors: %d/%d\n", i->stdinfd, i->termfd);
+                        return i;
+                      }
+                    }
+                    input_free_esctrie(&i->inputescapes);
+                  }
+                  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->inputescapes);
+    // do not kill the thread here, either.
+    if(i->initdata){
+      free(i->initdata->version);
+      free(i->initdata);
+    }
+    if(i->initdata_complete){
+      free(i->initdata_complete->version);
+      free(i->initdata_complete);
+    }
+    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* nc){
+  // we do not list here those already handled by prep_windows_special_keys()
+  static const struct {
+    const char* esc;
+    uint32_t key;
+    bool shift, ctrl, alt;
+  } 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, .shift = 1, },
+    { .esc = "\x1b[127;3u", .key = NCKEY_BACKSPACE, .alt = 1, },
+    { .esc = "\x1b[127;5u", .key = NCKEY_BACKSPACE, .ctrl = 1, },
+    { .esc = NULL, .key = NCKEY_INVALID, },
+  }, *k;
+  for(k = keys ; k->esc ; ++k){
+    if(inputctx_add_input_escape(nc, k->esc, k->key, k->shift, k->ctrl, k->alt)){
+      return -1;
+    }
+  }
+  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* nc){
+  // 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;
+    bool shift, ctrl, alt;
+  } 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, .ctrl = 1, },
+    { .esc = "\x1b[1;5B", .key = NCKEY_DOWN, .ctrl = 1, },
+    { .esc = "\x1b[1;5C", .key = NCKEY_RIGHT, .ctrl = 1, },
+    { .esc = "\x1b[1;5D", .key = NCKEY_LEFT, .ctrl = 1, },
+    { .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 = NCKEY_INVALID, },
+  }, *k;
+  for(k = keys ; k->esc ; ++k){
+    if(inputctx_add_input_escape(nc, k->esc, k->key, k->shift, k->ctrl, k->alt)){
+      return -1;
+    }
+  }
+  return 0;
+}
+
+static int
+prep_all_keys(inputctx* ictx){
+  if(prep_windows_special_keys(ictx)){
+    return -1;
+  }
+  if(prep_kitty_special_keys(ictx)){
+    input_free_esctrie(&ictx->inputescapes);
+    return -1;
+  }
+  ictx->triepos = ictx->inputescapes;
+  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){
+  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){
+      logwarn("couldn't read from %d (%s)\n", fd, strerror(errno));
+    }
+    return;
+  }
+  *bufused += r;
+  space -= r;
+  loginfo("read %lldB from %d (%lluB left)\n", (long long)r, fd, (unsigned long long)space);
+}
+
+// are terminal and stdin distinct for this inputctx?
+static inline bool
+ictx_independent_p(const inputctx* ictx){
+  return ictx->termfd >= 0; // FIXME does this hold on MSFT Terminal?
+}
+
+static int
+ruts_numeric(int* numeric, unsigned char c){
+  if(!isdigit(c)){
+    return -1;
+  }
+  int digit = c - '0';
+  if(INT_MAX / 10 - digit < *numeric){ // would overflow
+    return -1;
+  }
+  *numeric *= 10;
+  *numeric += digit;
+  return 0;
+}
+
+static int
+ruts_hex(int* numeric, unsigned char c){
+  if(!isxdigit(c)){
+    return -1;
+  }
+  int digit;
+  if(isdigit(c)){
+    digit = c - '0';
+  }else if(islower(c)){
+    digit = c - 'a' + 10;
+  }else if(isupper(c)){
+    digit = c - 'A' + 10;
+  }else{
+    return -1; // should be impossible to reach
+  }
+  if(INT_MAX / 10 - digit < *numeric){ // would overflow
+    return -1;
+  }
+  *numeric *= 16;
+  *numeric += digit;
+  return 0;
+}
+
+// add a decoded hex byte to the string
+static int
+ruts_string(inputctx* ictx, initstates_e state){
+  if(ictx->stridx == sizeof(ictx->runstring)){
+    return -1; // overflow, too long
+  }
+  if(ictx->numeric > 255){
+    return -1;
+  }
+  unsigned char c = ictx->numeric;
+  if(!isprint(c)){
+    return -1;
+  }
+  ictx->stringstate = state;
+  ictx->runstring[ictx->stridx] = c;
+  ictx->runstring[++ictx->stridx] = '\0';
+  return 0;
+}
+
+// extract the terminal version from the running string, following 'prefix'
+static int
+extract_version(inputctx* ictx, size_t slen){
+  size_t bytes = strlen(ictx->runstring + slen) + 1;
+  ictx->initdata->version = malloc(bytes);
+  if(ictx->initdata->version == NULL){
+    return -1;
+  }
+  memcpy(ictx->initdata->version, ictx->runstring + slen, bytes);
+  return 0;
+}
+
+static int
+extract_xtversion(inputctx* ictx, size_t slen, char suffix){
+  if(suffix){
+    if(ictx->runstring[ictx->stridx - 1] != suffix){
+      return -1;
+    }
+    ictx->runstring[ictx->stridx - 1] = '\0';
+  }
+  return extract_version(ictx, slen);
+}
+
+static int
+stash_string(inputctx* ictx){
+  struct initial_responses* inits = ictx->initdata;
+//fprintf(stderr, "string terminator after %d [%s]\n", inits->stringstate, inits->runstring);
+  if(inits){
+    switch(ictx->stringstate){
+      case STATE_XTVERSION1:{
+        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 = NULL, .suffix = 0, .term = TERMINAL_UNKNOWN, },
+        }, *xtv;
+        for(xtv = xtvers ; xtv->prefix ; ++xtv){
+          if(strncmp(ictx->runstring, xtv->prefix, strlen(xtv->prefix)) == 0){
+            if(extract_xtversion(ictx, strlen(xtv->prefix), xtv->suffix) == 0){
+              inits->qterm = xtv->term;
+            }
+            break;
+          }
+        }
+        if(xtv->prefix == NULL){
+          logwarn("Unrecognizable XTVERSION [%s]\n", ictx->runstring);
+        }
+        break;
+      }case STATE_XTGETTCAP_TERMNAME1:
+        if(strcmp(ictx->runstring, "xterm-kitty") == 0){
+          inits->qterm = TERMINAL_KITTY;
+        }else if(strcmp(ictx->runstring, "mlterm") == 0){
+          // MLterm prior to late 3.9.1 only reports via XTGETTCAP
+          inits->qterm = TERMINAL_MLTERM;
+        }
+        break;
+      case STATE_TDA1:
+        if(strcmp(ictx->runstring, "~VTE") == 0){
+          inits->qterm = TERMINAL_VTE;
+        }else if(strcmp(ictx->runstring, "~~TY") == 0){
+          inits->qterm = TERMINAL_TERMINOLOGY;
+        }else if(strcmp(ictx->runstring, "FOOT") == 0){
+          inits->qterm = TERMINAL_FOOT;
+        }
+        break;
+      case STATE_BG1:{
+        int r, g, b;
+        if(sscanf(ictx->runstring, "rgb:%02x/%02x/%02x", &r, &g, &b) == 3){
+          // great! =]
+        }else if(sscanf(ictx->runstring, "rgb:%04x/%04x/%04x", &r, &g, &b) == 3){
+          r /= 256;
+          g /= 256;
+          b /= 256;
+        }else{
+          break;
+        }
+        inits->bg = (r << 16u) | (g << 8u) | b;
+        break;
+      }default:
+  // don't generally enable this -- XTerm terminates TDA with ST
+  //fprintf(stderr, "invalid string [%s] stashed %d\n", inits->runstring, inits->stringstate);
+        break;
+    }
+  }
+  ictx->runstring[0] = '\0';
+  ictx->stridx = 0;
+  return 0;
+}
+
+// use the version extracted from Secondary Device Attributes, assuming that
+// it is Alacritty (we ought check the specified terminfo database entry).
+// Alacritty writes its crate version with each more significant portion
+// multiplied by 100^{portion ID}, where major, minor, patch are 2, 1, 0.
+// what happens when a component exceeds 99? who cares. support XTVERSION.
+static char*
+set_sda_version(inputctx* ictx){
+  int maj, min, patch;
+  if(ictx->numeric <= 0){
+    return NULL;
+  }
+  maj = ictx->numeric / 10000;
+  min = (ictx->numeric % 10000) / 100;
+  patch = ictx->numeric % 100;
+  if(maj >= 100 || min >= 100 || patch >= 100){
+    return NULL;
+  }
+  // 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);
+  }
+  return buf;
+}
+
+// ictx->numeric, ictx->p3, and ictx->p2 have the two parameters
+static void
+mouse_click(inputctx* ictx){
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logerror("dropping mouse click 0x%02x %d %d\n", ictx->p2, ictx->p3, ictx->numeric);
+    return;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  if(ictx->p2 >= 0 && ictx->p2 < 64){
+    if(ictx->p2 % 4 == 3){
+      ni->id = NCKEY_RELEASE;
+    }else{
+      ni->id = NCKEY_BUTTON1 + (ictx->p2 % 4);
+    }
+  }else if(ictx->p2 >= 64 && ictx->p2 < 128){
+    ni->id = NCKEY_BUTTON4 + (ictx->p2 % 4);
+  }else if(ictx->p2 >= 128 && ictx->p2 < 192){
+    ni->id = NCKEY_BUTTON8 + (ictx->p2 % 4);
+  }
+  ni->ctrl = ictx->p2 & 0x10;
+  ni->alt = ictx->p2 & 0x08;
+  ni->shift = ictx->p2 & 0x04;
+  // convert from 1- to 0-indexing and account for margins
+  ni->x = ictx->p3 - 1 - ictx->lmargin;
+  ni->y = ictx->numeric - 1 - ictx->tmargin;
+  if(++ictx->iwrite == ictx->isize){
+    ictx->iwrite = 0;
+  }
+  ++ictx->ivalid;
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+}
+
+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);
+}
+
+// add a decoded, valid Unicode to the bulk output buffer, or drop it if no
+// space is available.
+static void
+add_unicode(inputctx* ictx, uint32_t id){
+  inc_input_events(ictx);
+  if(ictx->drain){
+    return;
+  }
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logerror("dropping input 0x%08xx\n", id);
+    return;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  ni->id = id;
+  ni->alt = false;
+  ni->ctrl = false;
+  ni->shift = false;
+  ni->x = ni->y = 0;
+  if(++ictx->iwrite == ictx->isize){
+    ictx->iwrite = 0;
+  }
+  ++ictx->ivalid;
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+}
+
+static void
+alt_key(inputctx* ictx, unsigned id){
+  inc_input_events(ictx);
+  if(ictx->drain){
+    return;
+  }
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logerror("dropping input 0x%08xx\n", ictx->triepos->special);
+    return;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  ni->id = id;
+  ni->alt = true;
+  ni->ctrl = false;
+  ni->shift = false;
+  ni->x = ni->y = 0;
+  if(++ictx->iwrite == ictx->isize){
+    ictx->iwrite = 0;
+  }
+  ++ictx->ivalid;
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+}
+
+static void
+special_key(inputctx* ictx){
+  assert(ictx->triepos);
+  assert(NCKEY_INVALID != ictx->triepos->special);
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logerror("dropping input 0x%08xx\n", ictx->triepos->special);
+    return;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  ni->id = ictx->triepos->special;
+  ni->alt = ictx->triepos->alt;
+  ni->ctrl = ictx->triepos->ctrl;
+  ni->shift = ictx->triepos->shift;
+  ni->x = ni->y = 0;
+  if(++ictx->iwrite == ictx->isize){
+    ictx->iwrite = 0;
+  }
+  ++ictx->ivalid;
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+}
+
+// ictx->numeric and ictx->p2 have the two parameters
+static void
+kitty_kbd(inputctx* ictx){
+  enum { // synthesized events derived from keypresses
+    SYNTH_NOTHING,
+    SYNTH_SIGINT,
+    SYNTH_SIGQUIT,
+  } synth = SYNTH_NOTHING;
+  assert(ictx->numeric > 0);
+  assert(ictx->p2 > 0);
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logerror("dropping input 0x%08x 0x%02x\n", ictx->p2, ictx->numeric);
+    return;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  if((ni->id = ictx->p2) == 0x7f){
+    ni->id = NCKEY_BACKSPACE;
+  }
+  ni->shift = !!((ictx->numeric - 1) & 0x1);
+  ni->alt = !!((ictx->numeric - 1) & 0x2);
+  ni->ctrl = !!((ictx->numeric - 1) & 0x4);
+  // FIXME decode remaining modifiers through 128
+  // standard keyboard protocol reports ctrl+ascii as the capital form,
+  // so (for now) conform when using kitty protocol...
+  if(ni->id < 128 && islower(ni->id) && ni->ctrl){
+    ni->id = toupper(ni->id);
+  }
+  ni->x = 0;
+  ni->y = 0;
+  if(++ictx->iwrite == ictx->isize){
+    ictx->iwrite = 0;
+  }
+  ++ictx->ivalid;
+  if(ni->ctrl && !ni->alt && !ni->shift){
+    if(ni->id == 'C'){
+      synth = SYNTH_SIGINT;
+    }else if(ni->id == '\\'){
+      synth = SYNTH_SIGQUIT;
+    }
+  }
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+#ifndef __MINGW64__
+  if(synth == SYNTH_SIGINT){
+    raise(SIGINT);
+  }else if(synth == SYNTH_SIGQUIT){
+    raise(SIGQUIT);
+  }
+#endif
+}
+
+// FIXME ought implement the full Williams automaton
+// FIXME sloppy af in general
+// returns 1 after handling the Device Attributes response, 0 if more input
+// ought be fed to the machine, and -1 on an invalid state transition.
+static int
+pump_control_read(inputctx* ictx, unsigned char c){
+  logdebug("state: %2d char: %1c %3d %02x\n", ictx->state, isprint(c) ? c : ' ', c, c);
+  if(c == NCKEY_ESC){
+    ictx->state = STATE_ESC;
+    return 0;
+  }
+  switch(ictx->state){
+    case STATE_NULL:
+      // not an escape -- throw into user queue
+      break;
+    case STATE_ESC:
+      ictx->numeric = 0;
+      if(c == '['){
+        ictx->state = STATE_CSI;
+      }else if(c == 'P'){
+        ictx->state = STATE_DCS;
+      }else if(c == '\\'){
+        if(stash_string(ictx)){
+          return -1;
+        }
+        ictx->state = STATE_NULL;
+      }else if(c == '1'){
+        ictx->state = STATE_BG1;
+      }else if(c == '_'){
+        ictx->state = STATE_APC;
+      }
+      break;
+    case STATE_APC:
+      if(c == 'G'){
+        if(ictx->initdata){
+          ictx->initdata->kitty_graphics = true;
+        }
+      }
+      ictx->state = STATE_APC_DRAIN;
+      break;
+    case STATE_APC_DRAIN:
+      if(c == '\x1b'){
+        ictx->state = STATE_APC_ST;
+      }
+      break;
+    case STATE_APC_ST:
+      if(c == '\\'){
+        ictx->state = STATE_NULL;
+      }else{
+        ictx->state = STATE_APC_DRAIN;
+      }
+      break;
+    case STATE_BG1:
+      if(c == '1'){
+        ictx->state = STATE_BG2;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_BG2:
+      if(c == ';'){
+        ictx->state = STATE_BGSEMI;
+        ictx->stridx = 0;
+        ictx->runstring[0] = '\0';
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_BGSEMI: // drain string
+      if(c == '\x07'){ // contour sends this at the end for some unknown reason
+        if(stash_string(ictx)){
+          return -1;
+        }
+        ictx->state = STATE_NULL;
+        break;
+      }
+      ictx->numeric = c;
+      if(ruts_string(ictx, STATE_BG1)){
+        return -1;
+      }
+      break;
+    case STATE_CSI: // terminated by 0x40--0x7E ('@'--'~')
+      if(c == '?'){
+        ictx->state = STATE_DA; // could also be DECRPM/XTSMGRAPHICS/kittykbd
+      }else if(c == '>'){
+        // SDA yields up Alacritty's crate version, but it doesn't unambiguously
+        // identify Alacritty. If we've got any other version information, skip
+        // directly to STATE_SDA_DRAIN, rather than doing STATE_SDA_VER.
+        if(ictx->initdata){
+          if(ictx->initdata->qterm || ictx->initdata->version){
+            loginfo("Identified terminal already; ignoring DA2\n");
+            ictx->state = STATE_SDA_DRAIN;
+          }else{
+            ictx->state = STATE_SDA;
+          }
+        }
+      }else if(c == '<'){
+        ictx->state = STATE_MOUSE;
+      }else if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == ';'){
+        ictx->p2 = ictx->numeric;
+        ictx->state = STATE_CURSOR_COL;
+        ictx->numeric = 0;
+      }else if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_MOUSE:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == ';'){
+        ictx->state = STATE_MOUSE2;
+        ictx->p2 = ictx->numeric;
+        ictx->numeric = 0;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_MOUSE2:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == ';'){
+        ictx->state = STATE_MOUSE3;
+        ictx->p3 = ictx->numeric;
+        ictx->numeric = 0;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_MOUSE3:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 'M'){
+        mouse_click(ictx);
+        ictx->state = STATE_NULL;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_CURSOR_COL:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 'R'){
+//fprintf(stderr, "CURSOR X: %d\n", ictx->numeric);
+        if(ictx->initdata){
+          // convert from 1- to 0-indexing, and account for margins
+          ictx->initdata->cursorx = ictx->numeric - 1 - ictx->lmargin;
+          ictx->initdata->cursory = ictx->p2 - 1 - ictx->tmargin;
+        }else{
+          pthread_mutex_lock(&ictx->clock);
+          if(ictx->cvalid == ictx->csize){
+            pthread_mutex_unlock(&ictx->clock);
+            logwarn("dropping cursor location report\n");
+          }else{
+            cursorloc* cloc = &ictx->csrs[ictx->cwrite];
+            cloc->x = ictx->numeric - 1;
+            cloc->y = ictx->p2 - 1;
+            if(++ictx->cwrite == ictx->csize){
+              ictx->cwrite = 0;
+            }
+            ++ictx->cvalid;
+            pthread_mutex_unlock(&ictx->clock);
+            pthread_cond_broadcast(&ictx->ccond);
+          }
+        }
+        ictx->state = STATE_NULL;
+      }else if(c == 't'){
+//fprintf(stderr, "CELLS X: %d\n", ictx->numeric);
+        if(ictx->initdata){
+          ictx->initdata->dimx = ictx->numeric;
+          ictx->initdata->dimy = ictx->p2;
+        }
+        ictx->state = STATE_NULL;
+      }else if(c == 'u'){
+        // kitty keyboard protocol
+        kitty_kbd(ictx);
+        ictx->state = STATE_NULL;
+      }else if(c == ';'){
+        if(ictx->p2 == 4){
+          if(ictx->initdata){
+            ictx->initdata->pixy = ictx->numeric;
+            ictx->state = STATE_PIXELS_WIDTH;
+          }
+          ictx->numeric = 0;
+        }else if(ictx->p2 == 8){
+          if(ictx->initdata){
+            ictx->initdata->dimy = ictx->numeric;
+          }
+          ictx->state = STATE_CELLS_WIDTH;
+          ictx->numeric = 0;
+        }else{
+          logerror("expected 4 to lead pixel report, got %d\n", ictx->p2);
+          return -1;
+        }
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_PIXELS_WIDTH:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 't'){
+        if(ictx->initdata){
+          ictx->initdata->pixx = ictx->numeric;
+          loginfo("got pixel geometry: %d/%d\n", ictx->initdata->pixy, ictx->initdata->pixx);
+        }
+        ictx->state = STATE_NULL;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_CELLS_WIDTH:
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 't'){
+        if(ictx->initdata){
+          ictx->initdata->dimx = ictx->numeric;
+          loginfo("got cell geometry: %d/%d\n", ictx->initdata->dimy, ictx->initdata->dimx);
+        }
+        ictx->state = STATE_NULL;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_DCS: // terminated by ST
+      if(c == '\\'){
+//fprintf(stderr, "terminated DCS\n");
+        ictx->state = STATE_NULL;
+      }else if(c == '1'){
+        ictx->state = STATE_XTGETTCAP1; // we have tcap
+      }else if(c == '0'){
+        ictx->state = STATE_XTGETTCAP1; // no tcap for us
+      }else if(c == '>'){
+        ictx->state = STATE_XTVERSION1;
+      }else if(c == '!'){
+        ictx->state = STATE_TDA1;
+      }else{
+        ictx->state = STATE_DCS_DRAIN;
+      }
+      break;
+    case STATE_DCS_DRAIN:
+      // we drain to ST, which is an escape, and thus already handled, so...
+      break;
+    case STATE_XTVERSION1:
+      if(c == '|'){
+        ictx->state = STATE_XTVERSION2;
+        ictx->stridx = 0;
+        ictx->runstring[0] = '\0';
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_XTVERSION2:
+      ictx->numeric = c;
+      if(ruts_string(ictx, STATE_XTVERSION1)){
+        return -1;
+      }
+      break;
+    case STATE_XTGETTCAP1:
+      if(c == '+'){
+        ictx->state = STATE_XTGETTCAP2;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_XTGETTCAP2:
+      if(c == 'r'){
+        ictx->state = STATE_XTGETTCAP3;
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_XTGETTCAP3:
+      if(c == '='){
+        if(ictx->numeric == 0x544e){
+          ictx->state = STATE_XTGETTCAP_TERMNAME1;
+          ictx->stridx = 0;
+          ictx->numeric = 0;
+          ictx->runstring[0] = '\0';
+        }else{
+          ictx->state = STATE_DCS_DRAIN;
+        }
+      }else if(ruts_hex(&ictx->numeric, c)){
+        return -1;
+      }
+      break;
+    case STATE_XTGETTCAP_TERMNAME1:
+      if(ruts_hex(&ictx->numeric, c)){
+        return -1;
+      }
+      ictx->state = STATE_XTGETTCAP_TERMNAME2;
+      break;
+    case STATE_XTGETTCAP_TERMNAME2:
+      if(ruts_hex(&ictx->numeric, c)){
+        return -1;
+      }
+      ictx->state = STATE_XTGETTCAP_TERMNAME1;
+      if(ruts_string(ictx, STATE_XTGETTCAP_TERMNAME1)){
+        return -1;
+      }
+      ictx->numeric = 0;
+      break;
+    case STATE_TDA1:
+      if(c == '|'){
+        ictx->state = STATE_TDA2;
+        ictx->stridx = 0;
+        ictx->runstring[0] = '\0';
+      }else{
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_TDA2:
+      if(ruts_hex(&ictx->numeric, c)){
+        return -1;
+      }
+      ictx->state = STATE_TDA3;
+      break;
+    case STATE_TDA3:
+      if(ruts_hex(&ictx->numeric, c)){
+        return -1;
+      }
+      ictx->state = STATE_TDA2;
+      if(ruts_string(ictx, STATE_TDA1)){
+        ictx->state = STATE_DCS_DRAIN; // FIXME return -1?
+      }
+      ictx->numeric = 0;
+      break;
+    case STATE_SDA:
+      if(c == ';'){
+        ictx->state = STATE_SDA_VER;
+        ictx->numeric = 0;
+      }else if(c == 'c'){
+        ictx->state = STATE_NULL;
+      }
+      break;
+    case STATE_SDA_VER:
+      if(c == ';'){
+        ictx->state = STATE_SDA_DRAIN;
+        if(ictx->initdata){
+          loginfo("Got DA2 Pv: %u\n", ictx->numeric);
+          // if a version was set, we couldn't have arrived here. alacritty
+          // writes its crate version here, in an encoded form. nothing else
+          // necessarily does, though, so allow failure. this value will be
+          // interpreted as the version only if TERM indicates alacritty.
+          ictx->initdata->version = set_sda_version(ictx);
+        }
+      }else if(ruts_numeric(&ictx->numeric, c)){
+        return -1;
+      }
+      break;
+    case STATE_SDA_DRAIN:
+      if(c == 'c'){
+        ictx->state = STATE_NULL;
+      }
+      break;
+    // primary device attributes and XTSMGRAPHICS replies are generally
+    // indistinguishable until well into the escape. one can get:
+    // XTSMGRAPHICS: CSI ? Pi ; Ps ; Pv S {Pi: 123} {Ps: 0123}
+    // DECRPM: CSI ? Pd ; Ps $ y {Pd: many} {Ps: 01234}
+    // DA: CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
+    //     CSI ? 1 ; 0 c  ("VT101 with No Options")
+    //     CSI ? 4 ; 6 c  ("VT132 with Advanced Video and Graphics")
+    //     CSI ? 6 c  ("VT102")
+    //     CSI ? 7 c  ("VT131")
+    //     CSI ? 1 2 ; Ps c  ("VT125")
+    //     CSI ? 6 2 ; Ps c  ("VT220")
+    //     CSI ? 6 3 ; Ps c  ("VT320")
+    //     CSI ? 6 4 ; Ps c  ("VT420")
+    // KITTYKBD: CSI ? flags u
+    case STATE_DA: // return success on end of DA
+//fprintf(stderr, "DA: %c\n", c);
+      // FIXME several of these numbers could be DECRPM/XTSM/kittykbd. probably
+      // just want to read number, *then* make transition on non-number.
+      if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){ // stash for DECRPM/XTSM/kittykbd
+          return -1;
+        }
+      }else if(c == 'u'){ // kitty keyboard
+        loginfo("keyboard protocol level 0x%x\n", ictx->numeric);
+        ictx->state = STATE_NULL;
+      }else if(c == ';'){
+        ictx->p2 = ictx->numeric;
+        ictx->numeric = 0;
+        ictx->state = STATE_DA_SEMI;
+      }else if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+        if(c == 'c'){
+          return 1;
+        }
+      }
+      break;
+    case STATE_DA_SEMI:
+      if(c == ';'){
+        ictx->p3 = ictx->numeric;
+        ictx->numeric = 0;
+        ictx->state = STATE_DA_SEMI2;
+      }else if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == '$'){
+        if(ictx->p2 == 2026){
+          ictx->state = STATE_APPSYNC_REPORT;
+          loginfo("terminal reported SUM support\n");
+        }else{
+          ictx->state = STATE_APPSYNC_REPORT_DRAIN;
+        }
+      }else if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+        if(c == 'c'){
+          return 1;
+        }
+      }
+      break;
+    case STATE_DA_SEMI2:
+      if(c == ';'){
+        ictx->p4 = ictx->numeric;
+        ictx->numeric = 0;
+        ictx->state = STATE_DA_SEMI3;
+      }else if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 'S'){
+        if(ictx->p2 == 1){
+          if(ictx->initdata){
+            ictx->initdata->color_registers = ictx->numeric;
+            loginfo("sixel color registers: %d\n", ictx->initdata->color_registers);
+          }
+          ictx->numeric = 0;
+        }
+        ictx->state = STATE_NULL;
+      }else if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+        if(c == 'c'){
+          return 1;
+        }
+      }
+      break;
+    case STATE_DA_DRAIN:
+      if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+        if(c == 'c'){
+          return 1;
+        }
+      }
+      break;
+    case STATE_DA_SEMI3:
+      if(c == ';'){
+        ictx->numeric = 0;
+        ictx->state = STATE_DA_DRAIN;
+      }else if(isdigit(c)){
+        if(ruts_numeric(&ictx->numeric, c)){
+          return -1;
+        }
+      }else if(c == 'S'){
+        if(ictx->initdata){
+          ictx->initdata->sixelx = ictx->p4;
+          ictx->initdata->sixely = ictx->numeric;
+          loginfo("max sixel geometry: %dx%d\n", ictx->initdata->sixely, ictx->initdata->sixelx);
+        }
+      }else if(c >= 0x40 && c <= 0x7E){
+        ictx->state = STATE_NULL;
+        if(c == 'c'){
+          return 1;
+        }
+      }
+      break;
+    case STATE_APPSYNC_REPORT:
+      if(ictx->numeric == '2'){
+        if(ictx->initdata){
+          ictx->initdata->appsync_supported = 1;
+        }
+        ictx->state = STATE_APPSYNC_REPORT_DRAIN;
+      }
+      break;
+    case STATE_APPSYNC_REPORT_DRAIN:
+      if(c == 'y'){
+        ictx->state = STATE_NULL;
+      }
+      break;
+    default:
+      logerror("Reached invalid init state %d\n", ictx->state);
+      return -1;
+  }
+  return 0;
+}
+
+static void
+handoff_initial_responses(inputctx* ictx){
+  pthread_mutex_lock(&ictx->ilock);
+  ictx->initdata_complete = ictx->initdata;
+  ictx->initdata = NULL;
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+}
+
+// try to lex a control sequence off of buf. return the number of bytes
+// consumed if we do so, and -1 otherwise. buf is *not* necessarily
+// NUL-terminated. precondition: buflen >= 1.
+// FIXME we ought play complete failures into the general input buffer?
+static int
+process_escape(inputctx* ictx, const unsigned char* buf, int buflen){
+  int used = 0;
+  while(used < buflen){
+    int r = pump_control_read(ictx, buf[used]);
+    if(r == 1){
+      handoff_initial_responses(ictx);
+    }
+    // FIXME this drops alt+ characters intermingled with responses
+    if(ictx->initdata == NULL){
+      // an escape always resets the trie, as does a NULL transition
+      unsigned char candidate = buf[used];
+      if(candidate == NCKEY_ESC){
+        ictx->triepos = ictx->inputescapes;
+      }else if(ictx->triepos->trie[candidate] == NULL){
+        if(ictx->state == STATE_ESC){
+          if(candidate && candidate <= 0x80){ // FIXME what about supraASCII utf8?
+            alt_key(ictx, candidate);
+          }
+        }
+        ictx->triepos = ictx->inputescapes;
+      }else{
+        ictx->triepos = ictx->triepos->trie[candidate];
+        logtrace("triepos: %p in: %u special: 0x%08x\n", ictx->triepos, candidate, ictx->triepos->special);
+        if(ictx->triepos->special != NCKEY_INVALID){ // match! mark and reset
+          special_key(ictx);
+          ictx->triepos = ictx->inputescapes;
+        }else if(ictx->triepos->trie == NULL){
+          ictx->triepos = ictx->inputescapes;
+        }
+      }
+    }
+    ++used;
+  }
+  return 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.
+static void
+process_escapes(inputctx* ictx, unsigned char* buf, int* bufused){
+  int offset = 0;
+  while(*bufused){
+    int consumed = process_escape(ictx, buf + offset, *bufused);
+    if(consumed < 0){
+      break;
+    }
+    *bufused -= consumed;
+    offset += consumed;
+  }
+  // move any leftovers to the front; only happens if we fill output queue
+  if(*bufused){
+    memmove(buf, buf + offset, *bufused);
+  }
+}
+
+// precondition: buflen >= 1.
+static int
+process_input(const unsigned char* buf, int buflen, ncinput* ni){
+  memset(ni, 0, sizeof(*ni));
+  if(buf[0] == 0x7f){ // ASCII del, treated as backspace
+    ni->id = NCKEY_BACKSPACE;
+    return 1;
+  }
+  if(buf[0] < 0x80){ // pure ascii can't show up mid-utf8
+    if(buf[0] == '\n' || buf[0] == '\r'){
+      ni->id = NCKEY_ENTER;
+    }else if(buf[0] > 0 && buf[0] <= 26 && buf[0] != '\t'){
+      ni->id = buf[0] + 'A' - 1;
+      ni->ctrl = true;
+    }else{
+      ni->id = buf[0];
+    }
+    return 1;
+  }
+  int cpointlen = 0;
+  wchar_t w;
+  mbstate_t mbstate;
+  // FIXME verify that we have enough length based off first char
+  while(++cpointlen <= (int)MB_CUR_MAX && cpointlen < buflen){
+//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?
+    memset(&mbstate, 0, sizeof(mbstate));
+    size_t r;
+    if((r = mbrtowc(&w, (const char*)buf, cpointlen + 1, &mbstate)) != (size_t)-1 &&
+        r != (size_t)-2){
+      ni->id = w;
+      return cpointlen + 1;
+    }
+  }
+  // FIXME input error stat
+  return 0;
+}
+
+// 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){
+  pthread_mutex_lock(&ictx->ilock);
+  if(ictx->ivalid == ictx->isize){
+    pthread_mutex_unlock(&ictx->ilock);
+    logwarn("blocking on input output queue (%d+%d)\n", ictx->ivalid, buflen);
+    return 0;
+  }
+  ncinput* ni = ictx->inputs + ictx->iwrite;
+  int r = process_input(buf, buflen, ni);
+  if(r > 0){
+    inc_input_events(ictx);
+    if(!ictx->drain){
+      if(++ictx->iwrite == ictx->isize){
+        ictx->iwrite = 0;
+      }
+      ++ictx->ivalid;
+    }
+  }
+  pthread_mutex_unlock(&ictx->ilock);
+  pthread_cond_broadcast(&ictx->icond);
+  return r;
+}
+
+// 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. a control
+// sequence will be chopped up and passed up (assuming it to be valid UTF-8).
+static void
+process_bulk(inputctx* ictx, unsigned char* buf, int* bufused){
+  int offset = 0;
+  while(*bufused){
+    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/%d [0x%02x]\n", offset, *bufused, buf[offset]);
+    int consumed = 0;
+    if(buf[offset] == '\x1b'){
+      consumed = process_escape(ictx, buf + offset, *bufused);
+    }else{
+      consumed = process_ncinput(ictx, buf + offset, *bufused);
+    }
+    if(consumed < 0){
+      break;
+    }
+    *bufused -= consumed;
+    offset += consumed;
+  }
+}
+
+// walk the matching automaton from wherever we were.
+static void
+process_ibuf(inputctx* ictx){
+  if(resize_seen){
+    add_unicode(ictx, NCKEY_RESIZE);
+    resize_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);
+  }
+  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);
+      }
+    }
+  }
+}
+
+int ncinput_shovel(inputctx* ictx, const void* buf, int len){
+  process_melange(ictx, buf, &len);
+  if(len){
+    logwarn("dropping %d byte%s\n", len, len == 1 ? "" : "s");
+  }
+  return 0;
+}
+
+static int
+block_on_input(inputctx* ictx){
+  struct timespec* ts = NULL; // FIXME
+#ifdef __MINGW64__
+	int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1;
+	DWORD d = WaitForMultipleObjects(1, &ictx->stdinhandle, FALSE, timeoutms);
+	if(d == WAIT_TIMEOUT){
+		return 0;
+	}else if(d == WAIT_FAILED){
+		return -1;
+	}else if(d - WAIT_OBJECT_0 == 0){
+		return 1;
+	}
+	return -1;
+#else
+  int inevents = POLLIN;
+#ifdef POLLRDHUP
+  inevents |= POLLRDHUP;
+#endif
+  struct pollfd pfds[2] = {
+    {
+			.fd = ictx->stdinfd,
+			.events = inevents,
+			.revents = 0,
+    }
+  };
+  int pfdcount = 1;
+  if(ictx->termfd >= 0){
+    pfds[pfdcount].fd = ictx->termfd;
+    pfds[pfdcount].events = inevents;
+    pfds[pfdcount].revents = 0;
+    ++pfdcount;
+  }
+  int events;
+#if defined(__APPLE__) || defined(__MINGW64__)
+  int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1;
+  while((events = poll(pfds, pfdcount, timeoutms)) < 0){ // FIXME smask?
+#else
+  sigset_t smask;
+  sigfillset(&smask);
+  sigdelset(&smask, SIGCONT);
+  sigdelset(&smask, SIGWINCH);
+  while((events = ppoll(pfds, pfdcount, ts, &smask)) < 0){
+#endif
+    if(errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){
+      return -1;
+    }else if(errno == EINTR){
+      return resize_seen;
+    }
+  }
+  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){
+  // FIXME also need to wake up if our output queues have space that has
+  // opened up for us to write into, lest we deadlock with full buffers...
+  block_on_input(ictx);
+  // first we read from the terminal, if that's a distinct source.
+  read_input_nblock(ictx->termfd, ictx->tbuf, sizeof(ictx->tbuf),
+                    &ictx->tbufvalid);
+  // now read bulk, possibly with term escapes intermingled within (if there
+  // was not a distinct terminal source).
+  read_input_nblock(ictx->stdinfd, ictx->ibuf, sizeof(ictx->ibuf),
+                    &ictx->ibufvalid);
+}
+
+static void*
+input_thread(void* vmarshall){
+  inputctx* ictx = vmarshall;
+  prep_all_keys(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,
+                    ncsharedstats* stats, unsigned drain){
+  inputctx* ictx = create_inputctx(ti, infp, lmargin, tmargin, stats, drain);
+  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\n");
+  return 0;
+}
+
+int stop_inputlayer(tinfo* ti){
+  int ret = 0;
+  if(ti){
+    if(ti->ictx){
+      loginfo("tearing down input thread\n");
+      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;
+    }
+  }
+  return ret;
+}
+
+int inputready_fd(const inputctx* ictx){
+  return ictx->stdinfd;
+}
+
+static inline uint32_t
+internal_get(inputctx* ictx, const struct timespec* ts, ncinput* ni){
+  if(ictx->drain){
+    logerror("input is being drained\n");
+    return (uint32_t)-1;
+  }
+  pthread_mutex_lock(&ictx->ilock);
+  while(!ictx->ivalid){
+    if(ts == NULL){
+      pthread_cond_wait(&ictx->icond, &ictx->ilock);
+    }else{
+      if(pthread_cond_timedwait(&ictx->icond, &ictx->ilock, ts)){
+        if(errno == ETIMEDOUT){
+          return 0;
+        }
+        inc_input_errors(ictx);
+        return (uint32_t)-1;
+      }
+    }
+  }
+  memcpy(ni, &ictx->inputs[ictx->iread], sizeof(*ni));
+  if(++ictx->iread == ictx->isize){
+    ictx->iread = 0;
+  }
+  bool sendsignal = false;
+  if(ictx->ivalid-- == ictx->isize){
+    sendsignal = true;
+  }
+  ni->seqnum = ++ictx->seqnum;
+  pthread_mutex_unlock(&ictx->ilock);
+  if(sendsignal){
+    pthread_kill(ictx->tid, SIGCONT);
+  }
+  return ni->id;
+}
+
+// infp has already been set non-blocking
+uint32_t notcurses_get(notcurses* nc, const struct timespec* ts, ncinput* ni){
+  uint32_t r = internal_get(nc->tcache.ictx, ts, ni);
+  if(r != (uint32_t)-1){
+    ++nc->stats.s.input_events;
+  }
+  return r;
+}
+
+// FIXME better performance if we move this within the locked area
+int notcurses_getvec(notcurses* n, const struct timespec* ts,
+                     ncinput* ni, int vcount){
+  for(int v = 0 ; v < vcount ; ++v){
+    uint32_t u = notcurses_get(n, ts, &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* ts, ncinput* ni){
+  return internal_get(n->tcache.ictx, ts, ni);
+}
+
+uint32_t notcurses_getc(notcurses* nc, const struct timespec* ts,
+                        const void* unused, ncinput* ni){
+  (void)unused; // FIXME remove for abi3
+  return notcurses_get(nc, ts, ni);
+}
+
+uint32_t ncdirect_getc(ncdirect* nc, const struct timespec *ts,
+                       const void* unused, ncinput* ni){
+  (void)unused; // FIXME remove for abi3
+  return ncdirect_get(nc, ts, ni);
+}
+
+int get_cursor_location(struct inputctx* ictx, int* y, int* x){
+  pthread_mutex_lock(&ictx->clock);
+  while(ictx->cvalid == 0){
+    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\n");
+  }
+#ifndef __MINGW64__
+  if(ti->ttyfd < 0){
+    return 0;
+  }
+  struct termios tios;
+  if(tcgetattr(ti->ttyfd, &tios)){
+    logerror("Couldn't preserve terminal state for %d (%s)\n", ti->ttyfd, strerror(errno));
+    return -1;
+  }
+  tios.c_lflag &= ~ISIG;
+  if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
+    logerror("Error disabling signals on %d (%s)\n", ti->ttyfd, strerror(errno));
+    return -1;
+  }
+#else
+  DWORD mode;
+  if(!GetConsoleMode(ti->inhandle, &mode)){
+    logerror("error acquiring input mode\n");
+    return -1;
+  }
+  mode &= ~ENABLE_PROCESSED_INPUT;
+  if(!SetConsoleMode(ti->inhandle, mode)){
+    logerror("error setting input mode\n");
+    return -1;
+  }
+#endif
+  ti->ictx->linesigs = 0;
+  loginfo("disabled line discipline signals\n");
+  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\n");
+  }
+#ifndef __MINGW64__
+  if(ti->ttyfd < 0){
+    return 0;
+  }
+  struct termios tios;
+  if(tcgetattr(ti->ttyfd, &tios)){
+    logerror("Couldn't preserve terminal state for %d (%s)\n", ti->ttyfd, strerror(errno));
+    return -1;
+  }
+  tios.c_lflag |= ISIG;
+  if(tcsetattr(ti->ttyfd, TCSANOW, &tios)){
+    logerror("Error disabling signals on %d (%s)\n", ti->ttyfd, strerror(errno));
+    return -1;
+  }
+#else
+  DWORD mode;
+  if(!GetConsoleMode(ti->inhandle, &mode)){
+    logerror("error acquiring input mode\n");
+    return -1;
+  }
+  mode |= ENABLE_PROCESSED_INPUT;
+  if(!SetConsoleMode(ti->inhandle, mode)){
+    logerror("error setting input mode\n");
+    return -1;
+  }
+#endif
+  ti->ictx->linesigs = 1;
+  loginfo("enabled line discipline signals\n");
+  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_complete){
+    pthread_cond_wait(&ictx->icond, &ictx->ilock);
+  }
+  iresp = ictx->initdata_complete;
+  ictx->initdata_complete = NULL;
+  pthread_mutex_unlock(&ictx->ilock);
+  return iresp;
+}
diff --git a/src/lib/in.h b/src/lib/in.h
new file mode 100644
index 0000000000..324b74faae
--- /dev/null
+++ b/src/lib/in.h
@@ -0,0 +1,84 @@
+#ifndef NOTCURSES_IN
+#define NOTCURSES_IN
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// internal header, not installed
+
+#include <stdio.h>
+
+struct tinfo;
+struct inputctx;
+struct ncsharedstats;
+
+int init_inputlayer(struct tinfo* ti, FILE* infp, int lmargin, int tmargin,
+                    struct ncsharedstats* stats, unsigned drain)
+  __attribute__ ((nonnull (1, 2, 5)));
+
+int stop_inputlayer(struct tinfo* ti);
+
+int inputready_fd(const struct inputctx* ictx)
+  __attribute__ ((nonnull (1)));
+
+// allow another source provide raw input for distribution to client code.
+// drops input if there is no room in appropriate output queue.
+int ncinput_shovel(struct inputctx* ictx, const void* buf, int len)
+  __attribute__ ((nonnull (1, 2)));
+
+typedef enum {
+    TERMINAL_UNKNOWN,       // no useful information from queries; use termname
+    // the very limited linux VGA/serial console, or possibly the (deprecated,
+    // pixel-drawable, RGBA8888) linux framebuffer console. *not* fbterm.
+    TERMINAL_LINUX,         // ioctl()s
+    // the linux KMS/DRM console, *not* kmscon, but DRM direct dumb buffers
+    TERMINAL_LINUXDRM,      // ioctl()s
+    TERMINAL_XTERM,         // XTVERSION == 'XTerm(ver)'
+    TERMINAL_VTE,           // TDA: "~VTE"
+    TERMINAL_KITTY,         // XTGETTCAP['TN'] == 'xterm-kitty'
+    TERMINAL_FOOT,          // TDA: "\EP!|464f4f54\E\\"
+    TERMINAL_MLTERM,        // XTGETTCAP['TN'] == 'mlterm'
+    TERMINAL_TMUX,          // XTVERSION == "tmux ver"
+    TERMINAL_WEZTERM,       // XTVERSION == 'WezTerm *'
+    TERMINAL_ALACRITTY,     // can't be detected; match TERM+DA2
+    TERMINAL_CONTOUR,       // XTVERSION == 'contour ver'
+    TERMINAL_ITERM,         // XTVERSION == 'iTerm2 [ver]'
+    TERMINAL_TERMINOLOGY,   // TDA: "~~TY"
+    TERMINAL_APPLE,         // Terminal.App, determined by TERM_PROGRAM + macOS
+    TERMINAL_MSTERMINAL,    // Microsoft Windows Terminal
+    TERMINAL_MINTTY,        // XTVERSION == 'mintty ver' MinTTY (Cygwin, MSYS2)
+} queried_terminals_e;
+
+// after spawning the input layer, send initial queries to the terminal. its
+// responses will be built up herein. it's dangerous to go alone! take this!
+struct initial_responses {
+  int cursory;                 // cursor location
+  int cursorx;                 // cursor location
+  unsigned appsync_supported;  // is application-synchronized mode supported?
+  queried_terminals_e qterm;   // determined terminal
+  unsigned kitty_graphics;     // kitty graphics supported
+  uint32_t bg;                 // default background
+  int pixx;                    // screen geometry in pixels
+  int pixy;                    // screen geometry in pixels
+  int dimx;                    // screen geometry in cells
+  int dimy;                    // screen geometry in cells
+  int color_registers;         // sixel color registers
+  int sixely;                  // maximum sixel height
+  int sixelx;                  // maximum sixel width
+  char* version;               // version string, heap-allocated
+};
+
+// Blocking call. Waits until the input thread has processed all responses to
+// our initial queries, and returns them.
+struct initial_responses* inputlayer_get_responses(struct inputctx* ictx)
+  __attribute__ ((nonnull (1)));
+
+int get_cursor_location(struct inputctx* ictx, int* y, int* x)
+  __attribute__ ((nonnull (1)));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/lib/input.c b/src/lib/input.c
deleted file mode 100644
index 79106996bc..0000000000
--- a/src/lib/input.c
+++ /dev/null
@@ -1,1887 +0,0 @@
-#include "input.h"
-#include "internal.h"
-#include "notcurses/direct.h"
-#include <ctype.h>
-#include <signal.h>
-
-// CSI (Control Sequence Indicators) originate in the terminal itself, and are
-// not reported in their bare form to the user. For our purposes, these usually
-// indicate a mouse event or a cursor location report.
-#define CSIPREFIX "\x1b["
-static const uint32_t NCKEY_CSI = 0x90; // guaranteed not to match anything else
-
-static sig_atomic_t resize_seen;
-
-// called for SIGWINCH and SIGCONT
-void sigwinch_handler(int signo){
-  resize_seen = signo;
-}
-
-int cbreak_mode(tinfo* ti){
-#ifndef __MINGW64__
-  int ttyfd = ti->ttyfd;
-  if(ttyfd < 0){
-    return 0;
-  }
-  // assume it's not a true terminal (e.g. we might be redirected to a file)
-  struct termios modtermios;
-  memcpy(&modtermios, ti->tpreserved, sizeof(modtermios));
-  // see termios(3). disabling ECHO and ICANON means input will not be echoed
-  // to the screen, input is made available without enter-based buffering, and
-  // line editing is disabled. since we have not gone into raw mode, ctrl+c
-  // etc. still have their typical effects. ICRNL maps return to 13 (Ctrl+M)
-  // instead of 10 (Ctrl+J).
-  modtermios.c_lflag &= (~ECHO & ~ICANON);
-  modtermios.c_iflag &= ~ICRNL;
-  if(tcsetattr(ttyfd, TCSANOW, &modtermios)){
-    logerror("Error disabling echo / canonical on %d (%s)\n", ttyfd, strerror(errno));
-    return -1;
-  }
-#else
-  // we don't yet have a way to take Cygwin/MSYS2 out of canonical mode. we'll
-  // hit this stanza in MSYS2; allow the GetConsoleMode() to fail for now. this
-  // means we'll need enter pressed after the query response, obviously an
-  // unacceptable state of affairs...FIXME
-  DWORD mode;
-  if(!GetConsoleMode(ti->inhandle, &mode)){
-    logerror("error acquiring input mode\n");
-    return 0; // FIXME is it safe?
-  }
-  mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
-  if(!SetConsoleMode(ti->inhandle, mode)){
-    logerror("error setting input mode\n");
-    return -1;
-  }
-#endif
-  return 0;
-}
-
-// Disable signals originating from the terminal's line discipline, i.e.
-// SIGINT (^C), SIGQUIT (^\), and SIGTSTP (^Z). They are enabled by default.
-int notcurses_linesigs_disable(notcurses* n){
-#ifndef __MINGW64__
-  if(n->tcache.ttyfd < 0){
-    return 0;
-  }
-  struct termios tios;
-  if(tcgetattr(n->tcache.ttyfd, &tios)){
-    logerror("Couldn't preserve terminal state for %d (%s)\n", n->tcache.ttyfd, strerror(errno));
-    return -1;
-  }
-  tios.c_lflag &= ~ISIG;
-  if(tcsetattr(n->tcache.ttyfd, TCSANOW, &tios)){
-    logerror("Error disabling signals on %d (%s)\n", n->tcache.ttyfd, strerror(errno));
-    return -1;
-  }
-#else
-  DWORD mode;
-  if(!GetConsoleMode(n->tcache.inhandle, &mode)){
-    logerror("error acquiring input mode\n");
-    return -1;
-  }
-  mode &= ~ENABLE_PROCESSED_INPUT;
-  if(!SetConsoleMode(n->tcache.inhandle, mode)){
-    logerror("error setting input mode\n");
-    return -1;
-  }
-#endif
-  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){
-#ifndef __MINGW64__
-  if(n->tcache.ttyfd < 0){
-    return 0;
-  }
-  struct termios tios;
-  if(tcgetattr(n->tcache.ttyfd, &tios)){
-    logerror("Couldn't preserve terminal state for %d (%s)\n", n->tcache.ttyfd, strerror(errno));
-    return -1;
-  }
-  tios.c_lflag |= ~ISIG;
-  if(tcsetattr(n->tcache.ttyfd, TCSANOW, &tios)){
-    logerror("Error disabling signals on %d (%s)\n", n->tcache.ttyfd, strerror(errno));
-    return -1;
-  }
-#else
-  DWORD mode;
-  if(!GetConsoleMode(n->tcache.inhandle, &mode)){
-    logerror("error acquiring input mode\n");
-    return -1;
-  }
-  mode |= ENABLE_PROCESSED_INPUT;
-  if(!SetConsoleMode(n->tcache.inhandle, mode)){
-    logerror("error setting input mode\n");
-    return -1;
-  }
-#endif
-  return 0;
-}
-
-static inline int
-pop_input_keypress(ncinputlayer* nc){
-  int candidate = nc->inputbuf[nc->inputbuf_valid_starts];
-// fprintf(stderr, "DEOCCUPY: %u@%u read: %d\n", nc->inputbuf_occupied, nc->inputbuf_valid_starts, nc->inputbuf[nc->inputbuf_valid_starts]);
-  if(++nc->inputbuf_valid_starts == sizeof(nc->inputbuf) / sizeof(*nc->inputbuf)){
-    nc->inputbuf_valid_starts = 0;
-  }
-  --nc->inputbuf_occupied;
-  return candidate;
-}
-
-// assumes there is space, as you presumably just popped it
-static inline void
-unpop_keypress(ncinputlayer* nc, int kpress){
-  ++nc->inputbuf_occupied;
-  if(nc->inputbuf_valid_starts-- == 0){
-    nc->inputbuf_valid_starts = sizeof(nc->inputbuf) / sizeof(*nc->inputbuf) - 1;
-  }
-  nc->inputbuf[nc->inputbuf_valid_starts] = kpress;
-}
-
-// we assumed escapes can only be composed of 7-bit chars
-typedef struct esctrie {
-  struct esctrie** trie;  // if non-NULL, next level of radix-128 trie
-  uint32_t special;       // composed key terminating here
-  bool shift, ctrl, alt;
-} esctrie;
-
-static esctrie*
-create_esctrie_node(int special){
-  esctrie* e = malloc(sizeof(*e));
-  if(e){
-    e->special = special;
-    e->trie = NULL;
-    e->shift = 0;
-    e->ctrl = 0;
-    e->alt = 0;
-  }
-  return e;
-}
-
-static void
-input_free_esctrie(esctrie** eptr){
-  esctrie* e;
-  if( (e = *eptr) ){
-    if(e->trie){
-      int z;
-      for(z = 0 ; z < 0x80 ; ++z){
-        if(e->trie[z]){
-          input_free_esctrie(&e->trie[z]);
-        }
-      }
-      free(e->trie);
-    }
-    free(e);
-  }
-}
-
-// multiple input escapes might map to the same input
-static int
-ncinputlayer_add_input_escape(ncinputlayer* nc, const char* esc, uint32_t special,
-                              unsigned shift, unsigned ctrl, unsigned alt){
-  if(esc[0] != NCKEY_ESC || strlen(esc) < 2){ // assume ESC prefix + content
-    logerror("not an escape (0x%x)\n", special);
-    return -1;
-  }
-  esctrie** cur = &nc->inputescapes;
-  do{
-//fprintf(stderr, "ADDING: %s (%zu) for %d\n", esc, strlen(esc), special);
-    ++esc;
-    int validate = *esc;
-    if(validate < 0 || validate >= 0x80){
-      return -1;
-    }
-    if(*cur == NULL){
-      if((*cur = create_esctrie_node(NCKEY_INVALID)) == NULL){
-        return -1;
-      }
-    }
-    if(validate){
-      if((*cur)->trie == NULL){
-        const size_t tsize = sizeof((*cur)->trie) * 0x80;
-        if(((*cur)->trie = malloc(tsize)) == NULL){
-          return -1;
-        }
-        memset((*cur)->trie, 0, tsize);
-      }
-      cur = &(*cur)->trie[validate];
-    }
-  }while(*esc);
-  // it appears that multiple keys can be mapped to the same escape string. as
-  // an example, see "kend" and "kc1" in st ("simple term" from suckless) :/.
-  if((*cur)->special != NCKEY_INVALID){ // already had one here!
-    if((*cur)->special != special){
-      logwarn("already added escape (got 0x%x, wanted 0x%x)\n", (*cur)->special, special);
-    }
-  }else{
-    (*cur)->special = special;
-    (*cur)->shift = shift;
-    (*cur)->ctrl = ctrl;
-    (*cur)->alt = alt;
-  }
-  return 0;
-}
-
-// We received the CSI prefix. Extract the data payload. Right now, we handle
-// mouse and cursor location reports. The former is three parameters starting
-// with '<' and ending with 'm' or 'M'; the latter is two ending with 'R'.
-// Both use 1-biased coordinates, so a 0 can be safely rejected.
-static uint32_t
-handle_csi(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin){
-  // stash the first parameter away. it's encoded if the CSI ends up being a
-  // mouse event, and otherwise it's the cursor's column (x) coordinate.
-  int param1 = -1;
-  bool mouse = false;
-  enum {
-    PARAM1,  // reading first param (button + modifiers) plus delimiter
-    PARAM2,  // reading second param (x coordinate) plus delimiter
-    PARAM3,  // reading third param (y coordinate) plus terminator
-  } state = PARAM1;
-  int param = 0; // numeric translation of param thus far
-  uint32_t id = (uint32_t)-1;
-  while(nc->inputbuf_occupied){
-    int candidate = pop_input_keypress(nc);
-    logdebug("candidate: %c (%d)\n", candidate, candidate);
-    if(candidate == 'u'){ // kitty keyboard protocol
-      if(state == PARAM3){
-        logwarn("triparam kitty message?\n");
-        break;
-      }else if(state == PARAM1){
-        param1 = param;
-        param = 1;
-      }
-      ni->shift = !!((param - 1) & 0x1);
-      ni->alt = !!((param - 1) & 0x2);
-      ni->ctrl = !!((param - 1) & 0x4);
-      // FIXME decode remaining modifiers through 128
-      // standard keyboard protocol reports ctrl+ascii as the capital form,
-      // so (for now) conform with kitty protocol...
-      if(param1 < 128 && islower(param1) && ni->ctrl){
-        param1 = toupper(param1);
-      }
-      ni->id = param1;
-      return param1;
-    }
-    if(state == PARAM1){
-      // if !mouse and candidate is '>', set mouse. otherwise it ought be a
-      // digit or a semicolon.
-      if(candidate == '<'){
-        if(mouse){
-          break; // shouldn't see it twice
-        }
-        mouse = true;
-      }else if(candidate == ';'){
-        param1 = param;
-        state = PARAM2;
-        // modifiers: 32 (motion) 16 (control) 8 (alt) 4 (shift)
-        // buttons 4, 5, 6, 7: adds 64
-        // buttons 8, 9, 10, 11: adds 128
-        if(param >= 0 && param < 64){
-          if(param % 4 == 3){
-            id = NCKEY_RELEASE;
-          }else{
-            id = NCKEY_BUTTON1 + (param % 4);
-          }
-        }else if(param >= 64 && param < 128){
-          id = NCKEY_BUTTON4 + (param % 4);
-        }else if(param >= 128 && param < 192){
-          id = NCKEY_BUTTON8 + (param % 4);
-        }else{
-          break;
-        }
-        ni->ctrl = param & 0x10;
-        ni->alt = param & 0x08;
-        ni->shift = param & 0x04;
-        param = 0;
-      }else if(isdigit(candidate)){
-        param *= 10;
-        param += candidate - '0';
-      }else{
-        break;
-      }
-    }else if(state == PARAM2){
-      if(candidate == 'R'){ // cursor location report
-        if(mouse){
-          logwarn("Invalid mouse param (%d/%d)\n", param1, param);
-          break;
-        }
-        if(param <= 0 || param1 <= 0){
-          logwarn("Invalid cursor location param (%d/%d)\n", param1, param);
-          break;
-        }
-        logdebug("Cursor location report %d/%d\n", param1, param);
-        ni->x = param - 1;
-        ni->y = param1 - 1;
-        return NCKEY_CURSOR_LOCATION_REPORT;
-      }else if(candidate == ';'){
-        state = PARAM3;
-        if(param == 0){
-          logwarn("Invalid mouse param (%d/%d)\n", param1, param);
-          break;
-        }
-        ni->x = param - 1 - leftmargin;
-        param = 0;
-      }else if(isdigit(candidate)){
-        param *= 10;
-        param += candidate - '0';
-      }else{
-        break;
-      }
-    }else if(state == PARAM3){
-      if(candidate == 'm' || candidate == 'M'){
-        if(candidate == 'm'){
-          id = NCKEY_RELEASE;
-        }
-        if(param == 0){
-          logwarn("Invalid mouse param (%d/%d)\n", param1, param);
-          break;
-        }
-        ni->y = param - 1 - topmargin;
-        ni->id = id;
-        return id;
-      }else if(isdigit(candidate)){
-        param *= 10;
-        param += candidate - '0';
-      }else{
-        break;
-      }
-    }
-  }
-  // FIXME ungetc on failure! walk trie backwards or something
-  // FIXME increment the input_error stat
-  logerror("Error processing CSI (%d left)\n", nc->inputbuf_occupied);
-  return (uint32_t)-1;
-}
-
-// add the keypress we just read to our input queue (assuming there is room).
-// if there is a full UTF8 codepoint or keystroke (composed or otherwise),
-// return it, and pop it from the queue. if there is a full cursor location
-// report, push that to the location report queue.
-// we need to match escapes from terminfo, certainly, but what about
-// unexpected escapes? the williams automaton is our guide:
-// https://vt100.net/emu/dec_ansi_parser. here are the basics:
-//
-// six sets of input trump everything, setting a known state from all sources,
-// only two of which are directly available in 7-bit mode:
-//
-//  0x18/0x1a (cancel): ground state
-//  0x1b (escape): escape state
-//
-// the other four require two 7-bit characters:
-//
-//  0x90: DCS, also reached via 0x1b+0x50
-//  0x9b: CSI, also reached via 0x1b+0x5b
-//  0x9d: OSC, also reached via 0x1b+0x5d
-//  0x98/0x9e/0x9f: sos/pm/apc string, also reached via 0x1b+{0x58/0x5e/0x5f}
-//
-// if interrupted in the middle of an existing sequence, it must not be acted
-// upon, and should probably not be delivered as bulk input.
-//
-// CSI is most relevant to us here, since mouse and cursor reports arrive that
-// way. CSI is properly terminated by 0x40--0x7e. the parameter space covers
-// 0x30--0x39 and the delimiter 0x3b (';').
-static uint32_t
-handle_getc(ncinputlayer* nc, int kpress, ncinput* ni, int leftmargin, int topmargin){
-  if(kpress < 0){
-    return -1;
-  }
-  unsigned csiidx = 0;
-  if(kpress == NCKEY_ESC){
-    const esctrie* esc = nc->inputescapes;
-    int candidate = 0;
-    const esctrie* csi = NULL;
-    while(esc && nc->inputbuf_occupied){
-      if(esc->special != NCKEY_INVALID){
-        if(esc->special == NCKEY_CSI){
-          csi = esc;
-        }else{
-          break;
-        }
-      }
-      candidate = pop_input_keypress(nc);
-      logdebug("trie candidate: %c %d (%d)\n", candidate, esc->special, candidate);
-      if(csi){
-        nc->csibuf[csiidx++] = candidate;
-      }
-      if(esc->trie == NULL){
-        esc = NULL;
-      }else if(candidate >= 0x80 || candidate < 0){
-        esc = NULL;
-      }else{
-        esc = esc->trie[candidate];
-      }
-      logtrace("move to %p (%u)\n", esc, esc ? esc->special : 0);
-    }
-    if(esc && esc->special != NCKEY_INVALID){
-      ni->shift = esc->shift;
-      ni->ctrl = esc->ctrl;
-      ni->alt = esc->alt;
-      return esc->special;
-    }
-    if(csi){
-      while(csiidx){
-        unpop_keypress(nc, nc->csibuf[--csiidx]);
-      }
-      return handle_csi(nc, ni, leftmargin, topmargin);
-    }
-    // interpret it as alt + candidate FIXME broken for first char matching
-    // trie, second char not -- will read as alt+second char...
-    if(candidate > 0 && candidate < 0x80){
-      ni->alt = true;
-      return candidate;
-    }
-    // FIXME ungetc on failure! walk trie backwards or something
-  }
-  if(kpress == 0x7f){ // ASCII del, treated as backspace
-    return NCKEY_BACKSPACE;
-  }
-  if(kpress < 0x80){
-    return kpress;
-  }
-  char cpoint[MB_CUR_MAX + 1];
-  memset(cpoint, 0, sizeof(cpoint));
-  size_t cpointlen = 0;
-  cpoint[cpointlen] = kpress;
-  wchar_t w;
-  mbstate_t mbstate;
-  while(++cpointlen <= (size_t)MB_CUR_MAX && nc->inputbuf_occupied){
-    int candidate = pop_input_keypress(nc);
-    if(candidate < 0x80){
-      unpop_keypress(nc, candidate);
-    }
-    cpoint[cpointlen] = candidate;
-//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?
-    memset(&mbstate, 0, sizeof(mbstate));
-    size_t r;
-    if((r = mbrtowc(&w, cpoint, cpointlen + 1, &mbstate)) != (size_t)-1 &&
-        r != (size_t)-2){
-      return w;
-    }
-  }
-  ++nc->stats->s.input_errors;
-  return -1;
-}
-
-// blocks up through ts (infinite with NULL ts), returning number of events
-// (0 on timeout) or -1 on error/interruption.
-static int
-block_on_input(tinfo* ti, const struct timespec* ts){
-#ifdef __MINGW64__
-  if(ti->qterm == TERMINAL_MSTERMINAL){
-    int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1;
-    DWORD d = WaitForMultipleObjects(1, &ti->inhandle, FALSE, timeoutms);
-    if(d == WAIT_TIMEOUT){
-      return 0;
-    }else if(d == WAIT_FAILED){
-      return -1;
-    }else if(d - WAIT_OBJECT_0 == 0){
-      return 1;
-    }
-    return -1;
-  }
-  return 1; // ? FIXME
-#else
-  struct pollfd pfd = {
-    .fd = ti->input.infd,
-    .events = POLLIN,
-    .revents = 0,
-  };
-#ifdef POLLRDHUP
-  pfd.events |= POLLRDHUP;
-#endif
-  int events;
-#if defined(__APPLE__) || defined(__MINGW64__)
-  int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1;
-  while((events = poll(&pfd, 1, timeoutms)) < 0){ // FIXME smask?
-#else // linux, BSDs
-  sigset_t smask;
-  sigfillset(&smask);
-  sigdelset(&smask, SIGCONT);
-  sigdelset(&smask, SIGWINCH);
-  while((events = ppoll(&pfd, 1, ts, &smask)) < 0){
-#endif
-    if(errno != EINTR && errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){
-      return -1;
-    }
-    if(resize_seen){
-      return 1;
-    }
-  }
-  return events;
-#endif
-}
-
-static inline size_t
-input_queue_space(const ncinputlayer* nc){
-  return sizeof(nc->inputbuf) / sizeof(*nc->inputbuf) - nc->inputbuf_occupied;
-}
-
-static int
-enqueue_cursor_report(ncinputlayer* nc, const ncinput* ni){
-  cursorreport* clr = malloc(sizeof(*clr));
-  if(clr == NULL){
-    return -1;
-  }
-  clr->y = ni->y;
-  clr->x = ni->x;
-  clr->next = NULL;
-  // i don't think we ever want to have more than one here. we don't actually
-  // have any control logic which leads to multiple outstanding requests, so
-  // any that arrive are presumably garbage from the bulk input (and probably
-  // ought be returned to the user).
-  free(nc->creport_queue);
-  nc->creport_queue = clr;
-  return 0;
-}
-
-static uint32_t
-handle_queued_input(ncinputlayer* nc, ncinput* ni,
-                    int leftmargin, int topmargin){
-  ncinput nireal;
-  if(ni == NULL){
-    ni = &nireal;
-  }
-  uint32_t ret;
-  do{
-    // if there was some error in getc(), we still dole out the existing queue
-    if(nc->inputbuf_occupied == 0){
-      return -1;
-    }
-    memset(ni, 0, sizeof(*ni));
-    int r = pop_input_keypress(nc);
-    ret = handle_getc(nc, r, ni, leftmargin, topmargin);
-    if(ret != (uint32_t)-1){
-      ni->id = ret;
-    }
-    if(ret == NCKEY_CURSOR_LOCATION_REPORT){
-      enqueue_cursor_report(nc, ni);
-    }else if(ni->ctrl && !ni->shift && !ni->alt){
-#ifndef __MINGW64__
-      if(ret == 'C'){
-        raise(SIGINT); // FIXME only if linesigs aren't disabled
-        continue;
-      }else if(ret == 'Z'){
-        raise(SIGTSTP); // FIXME only if linesigs aren't disabled
-        continue;
-      }else if(ret == '\\'){
-        raise(SIGQUIT); // FIXME only if linesigs aren't disabled
-        continue;
-      }
-#endif
-    }
-  }while(ret == NCKEY_CURSOR_LOCATION_REPORT);
-  return ret;
-}
-
-int ncinput_shovel(ncinputlayer* ni, const char* buf, size_t len){
-  int ret = -1;
-  pthread_mutex_lock(&ni->lock);
-  size_t space = input_queue_space(ni);
-  if(len < space){
-    size_t spaceback = sizeof(ni->inputbuf) / sizeof(*ni->inputbuf) - ni->inputbuf_write_at;
-    if(spaceback > len){
-      spaceback = len;
-    }
-    memcpy(ni->inputbuf + ni->inputbuf_write_at, buf, spaceback);
-    len -= spaceback;
-    ni->inputbuf_write_at += spaceback;
-    if(len){
-      memcpy(ni->inputbuf, buf + spaceback, len);
-      ni->inputbuf_write_at = len;
-    }
-    ni->inputbuf_occupied += len + spaceback;
-    ret = 0;
-  }
-  pthread_mutex_unlock(&ni->lock);
-  if(ret < 0){
-    logwarn("dropped %lluB event\n", (long long unsigned)len);
-  }
-  return ret;
-}
-
-// this is the only function which actually reads, and it can be called from
-// either our context (looking for cursor reports) or the user's. all it does
-// is attempt to fill up the input ringbuffer, exiting either when that
-// condition is met, or when we get an EAGAIN. it does no processing.
-static int
-fill_buffer(ncinputlayer* nc){
-  ssize_t r = 0;
-  size_t rlen;
-//fprintf(stderr, "OCCUPY: %u@%u read: %d %zd\n", nc->inputbuf_occupied, nc->inputbuf_write_at, nc->inputbuf[nc->inputbuf_write_at], r);
-  if((rlen = input_queue_space(nc)) > 0){
-    // if we have at least as much available as we do room to the end, read
-    // only to the end. otherwise, read as much as we have available.
-    if(rlen >= sizeof(nc->inputbuf) / sizeof(*nc->inputbuf) - nc->inputbuf_write_at){
-      rlen = sizeof(nc->inputbuf) / sizeof(*nc->inputbuf) - nc->inputbuf_write_at;
-    }
-    if((r = read(nc->infd, nc->inputbuf + nc->inputbuf_write_at, rlen)) > 0){
-      nc->inputbuf_write_at += r;
-      if(nc->inputbuf_write_at == sizeof(nc->inputbuf) / sizeof(*nc->inputbuf)){
-        nc->inputbuf_write_at = 0;
-      }
-      nc->inputbuf_occupied += r;
-    }else if(r < 0){
-      if(errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK){
-        return -1;
-      }
-    }
-  }
-  return 0;
-}
-
-// user-mode call to actual input i/o, which will get the next character from
-// the input buffer.
-static uint32_t
-handle_input(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin){
-  fill_buffer(nc);
-  // highest priority is resize notifications, since they don't queue
-  if(resize_seen){
-    resize_seen = 0;
-    return NCKEY_SIGNAL;
-  }
-  return handle_queued_input(nc, ni, leftmargin, topmargin);
-}
-
-static uint32_t
-handle_ncinput(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin){
-  if(ni){
-    memset(ni, 0, sizeof(*ni));
-  }
-  uint32_t r = handle_input(nc, ni, leftmargin, topmargin);
-  // ctrl (*without* alt) + letter maps to [1..26], and is independent of shift
-  // FIXME need to distinguish between:
-  //  - Enter and ^J
-  //  - Tab and ^I
-  bool ctrl = r > 0 && r <= 26;
-  if(ctrl){
-    if(r == '\n' || r == '\r'){
-      r = NCKEY_ENTER;
-      ctrl = false;
-    }else if(r == '\t'){ // FIXME infocmp: ht=^I, use that
-      ctrl = false;
-    }else{
-      r += 'A' - 1;
-    }
-  }
-  if(ni){
-    ni->id = r;
-    if(ctrl){
-      ni->ctrl = true;
-    }
-    // FIXME set shift
-  }
-  return r;
-}
-
-// helper so we can do counter increment at a single location
-static inline uint32_t
-ncinputlayer_prestamp(tinfo* ti, const struct timespec *ts,
-                      ncinput* ni, int leftmargin, int topmargin){
-//fprintf(stderr, "PRESTAMP OCCUPADO: %d\n", nc->inputbuf_occupied);
-  ncinputlayer* nc = &ti->input;
-  if(nc->inputbuf_occupied){
-    return handle_queued_input(nc, ni, leftmargin, topmargin);
-  }
-  errno = 0;
-  if(block_on_input(ti, ts) > 0){
-//fprintf(stderr, "%d events from input!\n", events);
-    return handle_ncinput(nc, ni, leftmargin, topmargin);
-  }
-//fprintf(stderr, "ERROR: %d events from input!\n", events);
-  return -1;
-}
-
-// infp has already been set non-blocking
-uint32_t notcurses_get(notcurses* nc, const struct timespec* ts, ncinput* ni){
-  uint32_t r = ncinputlayer_prestamp(&nc->tcache, ts, ni,
-                                     nc->margin_l, nc->margin_t);
-  if(r != (uint32_t)-1){
-    uint64_t stamp = nc->tcache.input.input_events++; // need increment even if !ni
-    if(ni){
-      ni->seqnum = stamp;
-    }
-    ++nc->stats.s.input_events;
-  }
-  return r;
-}
-
-uint32_t notcurses_getc(notcurses* nc, const struct timespec* ts,
-                        const void* unused, ncinput* ni){
-  (void)unused; // FIXME remove for abi3
-  return notcurses_get(nc, ts, ni);
-}
-
-uint32_t ncdirect_get(struct ncdirect* n, const struct timespec* ts, ncinput* ni){
-  uint32_t r = ncinputlayer_prestamp(&n->tcache, ts, ni, 0, 0);
-  if(r != (uint32_t)-1){
-    uint64_t stamp = n->tcache.input.input_events++; // need increment even if !ni
-    if(ni){
-      ni->seqnum = stamp;
-    }
-  }
-  return r;
-}
-
-uint32_t ncdirect_getc(ncdirect* nc, const struct timespec *ts,
-                       const void* unused, ncinput* ni){
-  (void)unused; // FIXME remove for abi3
-  return ncdirect_get(nc, ts, ni);
-}
-
-// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
-static int
-prep_kitty_special_keys(ncinputlayer* nc){
-  static const struct {
-    const char* esc;
-    uint32_t key;
-    bool shift, ctrl, alt;
-  } 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, .shift = 1, },
-    { .esc = "\x1b[127;3u", .key = NCKEY_BACKSPACE, .alt = 1, },
-    { .esc = "\x1b[127;5u", .key = NCKEY_BACKSPACE, .ctrl = 1, },
-    { .esc = NULL, .key = NCKEY_INVALID, },
-  }, *k;
-  for(k = keys ; k->esc ; ++k){
-    if(ncinputlayer_add_input_escape(nc, k->esc, k->key, k->shift, k->ctrl, k->alt)){
-      return -1;
-    }
-  }
-  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(ncinputlayer* nc){
-  // 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;
-    bool shift, ctrl, alt;
-  } 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, .ctrl = 1, },
-    { .esc = "\x1b[1;5B", .key = NCKEY_DOWN, .ctrl = 1, },
-    { .esc = "\x1b[1;5C", .key = NCKEY_RIGHT, .ctrl = 1, },
-    { .esc = "\x1b[1;5D", .key = NCKEY_LEFT, .ctrl = 1, },
-    { .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 = NCKEY_INVALID, },
-  }, *k;
-  for(k = keys ; k->esc ; ++k){
-    if(ncinputlayer_add_input_escape(nc, k->esc, k->key, k->shift, k->ctrl, k->alt)){
-      return -1;
-    }
-  }
-  return 0;
-}
-
-// load all known special keys from terminfo, and build the input sequence trie
-static int
-prep_special_keys(ncinputlayer* nc){
-  static const struct {
-    const char* tinfo;
-    uint32_t key;
-    bool shift, ctrl, alt;
-  } keys[] = {
-    { .tinfo = "kcbt",  .key = '\t', .shift = true, },
-    { .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 = "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 = NCKEY_INVALID, }
-  }, *k;
-  for(k = keys ; k->tinfo ; ++k){
-    char* seq = tigetstr(k->tinfo);
-    if(seq == NULL || seq == (char*)-1){
-      loginfo("no terminfo declaration for %s\n", k->tinfo);
-      continue;
-    }
-    if(seq[0] != NCKEY_ESC || strlen(seq) < 2){ // assume ESC prefix + content
-      logwarn("invalid escape: %s (0x%x)\n", k->tinfo, k->key);
-      continue;
-    }
-    if(ncinputlayer_add_input_escape(nc, seq, k->key, k->shift, k->ctrl, k->alt)){
-      return -1;
-    }
-    logdebug("support for terminfo's %s: %s\n", k->tinfo, seq);
-  }
-  if(ncinputlayer_add_input_escape(nc, CSIPREFIX, NCKEY_CSI, 0, 0, 0)){
-    return -1;
-  }
-  return 0;
-}
-
-void ncinputlayer_stop(ncinputlayer* nilayer){
-  if(pthread_cond_destroy(&nilayer->creport_cond)){
-    logerror("Error destroying cqueue condvar\n");
-  }
-  if(pthread_mutex_destroy(&nilayer->lock)){
-    logerror("Error destroying mutex\n");
-  }
-  cursorreport* clr;
-  while( (clr = nilayer->creport_queue) ){
-    nilayer->creport_queue = clr->next;
-    free(clr);
-  }
-  if(nilayer->ttyfd >= 0){
-    close(nilayer->ttyfd);
-  }
-  // do *not* close infd; it's just a fileno extracted from stdin
-  input_free_esctrie(&nilayer->inputescapes);
-}
-
-typedef enum {
-  STATE_NULL,
-  STATE_ESC,  // escape; aborts any active sequence
-  STATE_CSI,  // control sequence introducer
-  STATE_DCS,  // device control string
-  // XTVERSION replies with DCS > | ... ST
-  STATE_XTVERSION1,
-  STATE_XTVERSION2,
-  // XTGETTCAP replies with DCS 1 + r for a good request, or 0 + r for bad
-  STATE_XTGETTCAP1, // XTGETTCAP, got '0/1' (DCS 0/1 + r Pt ST)
-  STATE_XTGETTCAP2, // XTGETTCAP, got '+' (DCS 0/1 + r Pt ST)
-  STATE_XTGETTCAP3, // XTGETTCAP, got 'r' (DCS 0/1 + r Pt ST)
-  STATE_XTGETTCAP_TERMNAME1, // got property 544E, 'TN' (terminal name) first hex nibble
-  STATE_XTGETTCAP_TERMNAME2, // got property 544E, 'TN' (terminal name) second hex nibble
-  STATE_DCS_DRAIN,  // throw away input until we hit escape
-  STATE_APC,        // application programming command, starts with \x1b_
-  STATE_APC_DRAIN,  // looking for \x1b
-  STATE_APC_ST,     // looking for ST
-  STATE_BG1,        // got '1'
-  STATE_BG2,        // got second '1'
-  STATE_BGSEMI,     // got '11;', draining string to ESC ST
-  STATE_TDA1, // tertiary DA, got '!'
-  STATE_TDA2, // tertiary DA, got '|', first hex nibble
-  STATE_TDA3, // tertiary DA, second hex nibble
-  STATE_SDA,  // secondary DA (CSI > Pp ; Pv ; Pc c)
-  STATE_SDA_VER,  // secondary DA, got semi, reading to next semi
-  STATE_SDA_DRAIN, // drain secondary DA to 'c'
-  STATE_DA,   // primary DA   (CSI ? ... c) OR XTSMGRAPHICS OR DECRPM or kittykbd
-  STATE_DA_DRAIN, // drain out the primary DA to an alpha
-  STATE_DA_SEMI,    // got first semicolon following numeric
-  STATE_DA_SEMI2,   // got second semicolon following numeric ; numeric
-  STATE_DA_SEMI3,   // got third semicolon following numeric ; numeric ; numeric
-  STATE_APPSYNC_REPORT, // got DECRPT ?2026
-  STATE_APPSYNC_REPORT_DRAIN, // drain out decrpt to 'y'
-  // cursor location report: CSI row ; col R
-  // text area pixel geometry: CSI 4 ; rows ; cols t
-  // text area cell geometry: CSI 8 ; rows ; cols t
-  // so we handle them the same until we hit either a second semicolon or an
-  // 'R' or 't'. at the second ';', we verify that the first variable was
-  // '4' or '8', and continue to 't' via STATE_{PIXELS,CELLS}_WIDTH.
-  STATE_CURSOR_OR_PIXELGEOM, // reading row of cursor location to ';'
-  STATE_CURSOR_COL,    // reading col of cursor location to 'R', 't', or ';'
-  STATE_PIXELS_WIDTH,  // reading text area width in pixels to ';'
-  STATE_CELLS_WIDTH,   // reading text area width in cells to ';'
-} initstates_e;
-
-typedef struct query_state {
-  struct tinfo* tcache;
-  // if the terminal unambiguously identifies itself in response to our
-  // queries, use that identification for advanced feature support.
-  queried_terminals_e qterm;
-  char* version;        // terminal version, if detected. heap-allocated.
-  // stringstate is the state at which this string was initialized, and can be
-  // one of STATE_XTVERSION1, STATE_XTGETTCAP_TERMNAME1, STATE_TDA1, and STATE_BG1
-  initstates_e state, stringstate;
-  int numeric;           // currently-lexed numeric
-  char runstring[80];    // running string
-  size_t stridx;         // position to write in string
-  uint32_t bg;           // queried default background or 0
-  int pixelwidth;        // screen width in pixels
-  int pixelheight;       // screen height in pixels
-  int dimx;              // screen width in cells
-  int dimy;              // screen height in cells
-  int cursor_y, cursor_x;// cursor location
-  int cursor_or_pixel;   // holding cell until we determine which state
-  int three;             // third param (XTSMGRAPHICS)
-  int four;              // fourth param (XTSMGRAPHICS)
-
-  bool xtgettcap_good;   // high when we've received DCS 1
-  bool appsync;          // application-synchronized updates advertised
-  bool kittygraphics;    // kitty graphics were advertised
-} query_state;
-
-static int
-ruts_numeric(int* numeric, unsigned char c){
-  if(!isdigit(c)){
-    return -1;
-  }
-  int digit = c - '0';
-  if(INT_MAX / 10 - digit < *numeric){ // would overflow
-    return -1;
-  }
-  *numeric *= 10;
-  *numeric += digit;
-  return 0;
-}
-
-static int
-ruts_hex(int* numeric, unsigned char c){
-  if(!isxdigit(c)){
-    return -1;
-  }
-  int digit;
-  if(isdigit(c)){
-    digit = c - '0';
-  }else if(islower(c)){
-    digit = c - 'a' + 10;
-  }else if(isupper(c)){
-    digit = c - 'A' + 10;
-  }else{
-    return -1; // should be impossible to reach
-  }
-  if(INT_MAX / 10 - digit < *numeric){ // would overflow
-    return -1;
-  }
-  *numeric *= 16;
-  *numeric += digit;
-  return 0;
-}
-
-// add a decoded hex byte to the string
-static int
-ruts_string(query_state* inits, initstates_e state){
-  if(inits->stridx == sizeof(inits->runstring)){
-    return -1; // overflow, too long
-  }
-  if(inits->numeric > 255){
-    return -1;
-  }
-  unsigned char c = inits->numeric;
-  if(!isprint(c)){
-    return -1;
-  }
-  inits->stringstate = state;
-  inits->runstring[inits->stridx] = c;
-  inits->runstring[++inits->stridx] = '\0';
-  return 0;
-}
-
-// extract the terminal version from the running string, following 'prefix'
-static int
-extract_version(query_state* qstate, size_t slen){
-  size_t bytes = strlen(qstate->runstring + slen) + 1;
-  qstate->version = malloc(bytes);
-  if(qstate->version == NULL){
-    return -1;
-  }
-  memcpy(qstate->version, qstate->runstring + slen, bytes);
-  return 0;
-}
-
-static int
-extract_xtversion(query_state* qstate, size_t slen, char suffix){
-  if(suffix){
-    if(qstate->runstring[qstate->stridx - 1] != suffix){
-      return -1;
-    }
-    qstate->runstring[qstate->stridx - 1] = '\0';
-  }
-  return extract_version(qstate, slen);
-}
-
-static int
-stash_string(query_state* inits){
-//fprintf(stderr, "string terminator after %d [%s]\n", inits->stringstate, inits->runstring);
-  switch(inits->stringstate){
-    case STATE_XTVERSION1:{
-      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 = NULL, .suffix = 0, .term = TERMINAL_UNKNOWN, },
-      }, *xtv;
-      for(xtv = xtvers ; xtv->prefix ; ++xtv){
-        if(strncmp(inits->runstring, xtv->prefix, strlen(xtv->prefix)) == 0){
-          if(extract_xtversion(inits, strlen(xtv->prefix), xtv->suffix) == 0){
-            inits->qterm = xtv->term;
-          }
-          break;
-        }
-      }
-      if(xtv->prefix == NULL){
-        logwarn("Unrecognizable XTVERSION [%s]\n", inits->runstring);
-      }
-      break;
-    }case STATE_XTGETTCAP_TERMNAME1:
-      if(strcmp(inits->runstring, "xterm-kitty") == 0){
-        inits->qterm = TERMINAL_KITTY;
-      }else if(strcmp(inits->runstring, "mlterm") == 0){
-        // MLterm prior to late 3.9.1 only reports via XTGETTCAP
-        inits->qterm = TERMINAL_MLTERM;
-      }
-      break;
-    case STATE_TDA1:
-      if(strcmp(inits->runstring, "~VTE") == 0){
-        inits->qterm = TERMINAL_VTE;
-      }else if(strcmp(inits->runstring, "~~TY") == 0){
-        inits->qterm = TERMINAL_TERMINOLOGY;
-      }else if(strcmp(inits->runstring, "FOOT") == 0){
-        inits->qterm = TERMINAL_FOOT;
-      }
-      break;
-    case STATE_BG1:{
-      int r, g, b;
-      if(sscanf(inits->runstring, "rgb:%02x/%02x/%02x", &r, &g, &b) == 3){
-        // great! =]
-      }else if(sscanf(inits->runstring, "rgb:%04x/%04x/%04x", &r, &g, &b) == 3){
-        r /= 256;
-        g /= 256;
-        b /= 256;
-      }else{
-        break;
-      }
-      inits->bg = (r << 16u) | (g << 8u) | b;
-      break;
-    }default:
-// don't generally enable this -- XTerm terminates TDA with ST
-//fprintf(stderr, "invalid string [%s] stashed %d\n", inits->runstring, inits->stringstate);
-      break;
-  }
-  inits->runstring[0] = '\0';
-  inits->stridx = 0;
-  return 0;
-}
-
-// use the version extracted from Secondary Device Attributes, assuming that
-// it is Alacritty (we ought check the specified terminfo database entry).
-// Alacritty writes its crate version with each more significant portion
-// multiplied by 100^{portion ID}, where major, minor, patch are 2, 1, 0.
-// what happens when a component exceeds 99? who cares. support XTVERSION.
-static char*
-set_sda_version(query_state* inits){
-  int maj, min, patch;
-  if(inits->numeric <= 0){
-    return NULL;
-  }
-  maj = inits->numeric / 10000;
-  min = (inits->numeric % 10000) / 100;
-  patch = inits->numeric % 100;
-  if(maj >= 100 || min >= 100 || patch >= 100){
-    return NULL;
-  }
-  // 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);
-  }
-  return buf;
-}
-
-// FIXME ought implement the full Williams automaton
-// FIXME sloppy af in general
-// returns 1 after handling the Device Attributes response, 0 if more input
-// ought be fed to the machine, and -1 on an invalid state transition.
-static int
-pump_control_read(query_state* inits, unsigned char c){
-  logdebug("state: %2d char: %1c %3d %02x\n", inits->state, isprint(c) ? c : ' ', c, c);
-  if(c == NCKEY_ESC){
-    inits->state = STATE_ESC;
-    return 0;
-  }
-  switch(inits->state){
-    case STATE_NULL:
-      // not an escape -- throw into user queue
-      break;
-    case STATE_ESC:
-      inits->numeric = 0;
-      if(c == '['){
-        inits->state = STATE_CSI;
-      }else if(c == 'P'){
-        inits->state = STATE_DCS;
-      }else if(c == '\\'){
-        if(stash_string(inits)){
-          return -1;
-        }
-        inits->state = STATE_NULL;
-      }else if(c == '1'){
-        inits->state = STATE_BG1;
-      }else if(c == '_'){
-        inits->state = STATE_APC;
-      }
-      break;
-    case STATE_APC:
-      if(c == 'G'){
-        inits->kittygraphics = true;
-      }
-      inits->state = STATE_APC_DRAIN;
-      break;
-    case STATE_APC_DRAIN:
-      if(c == '\x1b'){
-        inits->state = STATE_APC_ST;
-      }
-      break;
-    case STATE_APC_ST:
-      if(c == '\\'){
-        inits->state = STATE_NULL;
-      }else{
-        inits->state = STATE_APC_DRAIN;
-      }
-      break;
-    case STATE_BG1:
-      if(c == '1'){
-        inits->state = STATE_BG2;
-      }else{
-        // FIXME
-      }
-      break;
-    case STATE_BG2:
-      if(c == ';'){
-        inits->state = STATE_BGSEMI;
-        inits->stridx = 0;
-        inits->runstring[0] = '\0';
-      }else{
-        // FIXME
-      }
-      break;
-    case STATE_BGSEMI: // drain string
-      if(c == '\x07'){ // contour sends this at the end for some unknown reason
-        if(stash_string(inits)){
-          return -1;
-        }
-        inits->state = STATE_NULL;
-        break;
-      }
-      inits->numeric = c;
-      if(ruts_string(inits, STATE_BG1)){
-        return -1;
-      }
-      break;
-    case STATE_CSI: // terminated by 0x40--0x7E ('@'--'~')
-      if(c == '?'){
-        inits->state = STATE_DA; // could also be DECRPM/XTSMGRAPHICS/kittykbd
-      }else if(c == '>'){
-        // SDA yields up Alacritty's crate version, but it doesn't unambiguously
-        // identify Alacritty. If we've got any other version information, skip
-        // directly to STATE_SDA_DRAIN, rather than doing STATE_SDA_VER.
-        if(inits->qterm || inits->version){
-          loginfo("Identified terminal already; ignoring DA2\n");
-          inits->state = STATE_SDA_DRAIN;
-        }else{
-          inits->state = STATE_SDA;
-        }
-      }else if(isdigit(c)){
-        inits->numeric = 0;
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-        inits->state = STATE_CURSOR_OR_PIXELGEOM;
-      }else if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_CURSOR_OR_PIXELGEOM:
-      if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == ';'){
-        inits->cursor_or_pixel = inits->numeric;
-        inits->state = STATE_CURSOR_COL;
-        inits->numeric = 0;
-      }else{
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_CURSOR_COL:
-      if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == 'R'){
-//fprintf(stderr, "CURSOR X: %d\n", inits->numeric);
-        inits->cursor_x = inits->numeric;
-        inits->cursor_y = inits->cursor_or_pixel;
-        inits->state = STATE_NULL;
-      }else if(c == 't'){
-//fprintf(stderr, "CELLS X: %d\n", inits->numeric);
-        // FIXME get these back to the caller
-        inits->dimx = inits->numeric;
-        inits->dimy = inits->cursor_or_pixel;
-        inits->state = STATE_NULL;
-      }else if(c == ';'){
-        if(inits->cursor_or_pixel == 4){
-          inits->pixelheight = inits->numeric;
-          inits->state = STATE_PIXELS_WIDTH;
-          inits->numeric = 0;
-        }else if(inits->cursor_or_pixel == 8){
-          inits->dimy = inits->numeric;
-          inits->state = STATE_CELLS_WIDTH;
-          inits->numeric = 0;
-        }else{
-          logerror("expected 4 to lead pixel report, got %d\n", inits->cursor_or_pixel);
-          return -1;
-        }
-      }else{
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_PIXELS_WIDTH:
-      if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == 't'){
-        inits->pixelwidth = inits->numeric;
-        loginfo("got pixel geometry: %d/%d\n", inits->pixelheight, inits->pixelwidth);
-        inits->state = STATE_NULL;
-      }else{
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_CELLS_WIDTH:
-      if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == 't'){
-        inits->dimx = inits->numeric;
-        loginfo("got cell geometry: %d/%d\n", inits->dimy, inits->dimx);
-        inits->state = STATE_NULL;
-      }else{
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_DCS: // terminated by ST
-      if(c == '\\'){
-//fprintf(stderr, "terminated DCS\n");
-        inits->state = STATE_NULL;
-      }else if(c == '1'){
-        inits->state = STATE_XTGETTCAP1;
-        inits->xtgettcap_good = true;
-      }else if(c == '0'){
-        inits->state = STATE_XTGETTCAP1;
-        inits->xtgettcap_good = false;
-      }else if(c == '>'){
-        inits->state = STATE_XTVERSION1;
-      }else if(c == '!'){
-        inits->state = STATE_TDA1;
-      }else{
-        inits->state = STATE_DCS_DRAIN;
-      }
-      break;
-    case STATE_DCS_DRAIN:
-      // we drain to ST, which is an escape, and thus already handled, so...
-      break;
-    case STATE_XTVERSION1:
-      if(c == '|'){
-        inits->state = STATE_XTVERSION2;
-        inits->stridx = 0;
-        inits->runstring[0] = '\0';
-      }else{
-        // FIXME error?
-      }
-      break;
-    case STATE_XTVERSION2:
-      inits->numeric = c;
-      if(ruts_string(inits, STATE_XTVERSION1)){
-        return -1;
-      }
-      break;
-    case STATE_XTGETTCAP1:
-      if(c == '+'){
-        inits->state = STATE_XTGETTCAP2;
-      }else{
-        // FIXME malformed
-      }
-      break;
-    case STATE_XTGETTCAP2:
-      if(c == 'r'){
-        inits->state = STATE_XTGETTCAP3;
-      }else{
-        // FIXME malformed
-      }
-      break;
-    case STATE_XTGETTCAP3:
-      if(c == '='){
-        if(inits->numeric == 0x544e){
-          inits->state = STATE_XTGETTCAP_TERMNAME1;
-          inits->stridx = 0;
-          inits->numeric = 0;
-          inits->runstring[0] = '\0';
-        }else{
-          inits->state = STATE_DCS_DRAIN;
-        }
-      }else if(ruts_hex(&inits->numeric, c)){
-        return -1;
-      }
-      break;
-    case STATE_XTGETTCAP_TERMNAME1:
-      if(ruts_hex(&inits->numeric, c)){
-        return -1;
-      }
-      inits->state = STATE_XTGETTCAP_TERMNAME2;
-      break;
-    case STATE_XTGETTCAP_TERMNAME2:
-      if(ruts_hex(&inits->numeric, c)){
-        return -1;
-      }
-      inits->state = STATE_XTGETTCAP_TERMNAME1;
-      if(ruts_string(inits, STATE_XTGETTCAP_TERMNAME1)){
-        return -1;
-      }
-      inits->numeric = 0;
-      break;
-    case STATE_TDA1:
-      if(c == '|'){
-        inits->state = STATE_TDA2;
-        inits->stridx = 0;
-        inits->runstring[0] = '\0';
-      }else{
-        // FIXME
-      }
-      break;
-    case STATE_TDA2:
-      if(ruts_hex(&inits->numeric, c)){
-        return -1;
-      }
-      inits->state = STATE_TDA3;
-      break;
-    case STATE_TDA3:
-      if(ruts_hex(&inits->numeric, c)){
-        return -1;
-      }
-      inits->state = STATE_TDA2;
-      if(ruts_string(inits, STATE_TDA1)){
-        inits->state = STATE_DCS_DRAIN; // FIXME return -1?
-      }
-      inits->numeric = 0;
-      break;
-    case STATE_SDA:
-      if(c == ';'){
-        inits->state = STATE_SDA_VER;
-        inits->numeric = 0;
-      }else if(c == 'c'){
-        inits->state = STATE_NULL;
-      }
-      break;
-    case STATE_SDA_VER:
-      if(c == ';'){
-        inits->state = STATE_SDA_DRAIN;
-        loginfo("Got DA2 Pv: %u\n", inits->numeric);
-        // if a version was set, we couldn't have arrived here. alacritty
-        // writes its crate version here, in an encoded form. nothing else
-        // necessarily does, though, so allow failure. this value will be
-        // interpreted as the version only if TERM indicates alacritty.
-        inits->version = set_sda_version(inits);
-      }else if(ruts_numeric(&inits->numeric, c)){
-        return -1;
-      }
-      break;
-    case STATE_SDA_DRAIN:
-      if(c == 'c'){
-        inits->state = STATE_NULL;
-      }
-      break;
-    // primary device attributes and XTSMGRAPHICS replies are generally
-    // indistinguishable until well into the escape. one can get:
-    // XTSMGRAPHICS: CSI ? Pi ; Ps ; Pv S {Pi: 123} {Ps: 0123}
-    // DECRPM: CSI ? Pd ; Ps $ y {Pd: many} {Ps: 01234}
-    // DA: CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
-    //     CSI ? 1 ; 0 c  ("VT101 with No Options")
-    //     CSI ? 4 ; 6 c  ("VT132 with Advanced Video and Graphics")
-    //     CSI ? 6 c  ("VT102")
-    //     CSI ? 7 c  ("VT131")
-    //     CSI ? 1 2 ; Ps c  ("VT125")
-    //     CSI ? 6 2 ; Ps c  ("VT220")
-    //     CSI ? 6 3 ; Ps c  ("VT320")
-    //     CSI ? 6 4 ; Ps c  ("VT420")
-    // KITTYKBD: CSI ? flags u
-    case STATE_DA: // return success on end of DA
-//fprintf(stderr, "DA: %c\n", c);
-      // FIXME several of these numbers could be DECRPM/XTSM/kittykbd. probably
-      // just want to read number, *then* make transition on non-number.
-      if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){ // stash for DECRPM/XTSM/kittykbd
-          return -1;
-        }
-      }else if(c == 'u'){ // kitty keyboard
-        inits->tcache->kittykbd = inits->numeric;
-        loginfo("keyboard protocol 0x%x\n", inits->tcache->kittykbd);
-        inits->state = STATE_NULL;
-      }else if(c == ';'){
-        inits->cursor_or_pixel = inits->numeric;
-        inits->numeric = 0;
-        inits->state = STATE_DA_SEMI;
-      }else if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-        if(c == 'c'){
-          return 1;
-        }
-      }
-      break;
-    case STATE_DA_SEMI:
-      if(c == ';'){
-        inits->three = inits->numeric;
-        inits->numeric = 0;
-        inits->state = STATE_DA_SEMI2;
-      }else if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == '$'){
-        if(inits->cursor_or_pixel == 2026){
-          inits->state = STATE_APPSYNC_REPORT;
-          loginfo("terminal reported SUM support\n");
-        }else{
-          inits->state = STATE_APPSYNC_REPORT_DRAIN;
-        }
-      }else if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-        if(c == 'c'){
-          return 1;
-        }
-      }
-      break;
-    case STATE_DA_SEMI2:
-      if(c == ';'){
-        inits->four = inits->numeric;
-        inits->numeric = 0;
-        inits->state = STATE_DA_SEMI3;
-      }else if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == 'S'){
-        if(inits->cursor_or_pixel == 1){
-          inits->tcache->color_registers = inits->numeric;
-          loginfo("sixel color registers: %d\n", inits->tcache->color_registers);
-          inits->numeric = 0;
-        }
-        inits->state = STATE_NULL;
-      }else if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-        if(c == 'c'){
-          return 1;
-        }
-      }
-      break;
-    case STATE_DA_DRAIN:
-      if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-        if(c == 'c'){
-          return 1;
-        }
-      }
-      break;
-    case STATE_DA_SEMI3:
-      if(c == ';'){
-        inits->numeric = 0;
-        inits->state = STATE_DA_DRAIN;
-      }else if(isdigit(c)){
-        if(ruts_numeric(&inits->numeric, c)){
-          return -1;
-        }
-      }else if(c == 'S'){
-        inits->tcache->sixel_maxx = inits->four;
-        inits->tcache->sixel_maxy = inits->numeric;
-        loginfo("max sixel geometry: %dx%d\n", inits->tcache->sixel_maxy, inits->tcache->sixel_maxx);
-      }else if(c >= 0x40 && c <= 0x7E){
-        inits->state = STATE_NULL;
-        if(c == 'c'){
-          return 1;
-        }
-      }
-      break;
-    case STATE_APPSYNC_REPORT:
-      if(inits->numeric == '2'){
-        inits->appsync = 1;
-        inits->state = STATE_APPSYNC_REPORT_DRAIN;
-      }
-      break;
-    case STATE_APPSYNC_REPORT_DRAIN:
-      if(c == 'y'){
-        inits->state = STATE_NULL;
-      }
-      break;
-    default:
-      fprintf(stderr, "Reached invalid init state %d\n", inits->state);
-      return -1;
-  }
-  return 0;
-}
-
-// complete the terminal detection process
-static int
-control_read(int ttyfd, query_state* qstate){
-  unsigned char* buf;
-  ssize_t s;
-
-  if((buf = malloc(BUFSIZ)) == NULL){
-    return -1;
-  }
-  errno = 0;
-  do{
-    loginfo("reading replies from %d...\n", ttyfd);
-    while((s = read(ttyfd, buf, BUFSIZ)) != -1){
-      loginfo("read %lld from %d\n", (long long)s, ttyfd);
-      for(ssize_t idx = 0; idx < s ; ++idx){
-        int r = pump_control_read(qstate, buf[idx]);
-        if(r == 1){ // success!
-          free(buf);
-//fprintf(stderr, "at end, derived terminal %d\n", qstate->qterm);
-          return 0;
-        }else if (r < 0 && (errno != EINTR && errno != EAGAIN && errno != EBUSY && errno != EWOULDBLOCK)){
-          goto err;
-        }
-      }
-    }
-  }while(errno == EINTR || errno == EAGAIN || errno == EBUSY || errno == EWOULDBLOCK);
-err:
-  fprintf(stderr, "Reading control replies failed on %d (%s)\n", ttyfd, strerror(errno));
-  free(buf);
-  return -1;
-}
-
-static int
-prep_all_keys(ncinputlayer* ni){
-  if(prep_windows_special_keys(ni)){
-    return -1;
-  }
-  if(prep_kitty_special_keys(ni)){
-    input_free_esctrie(&ni->inputescapes);
-    return -1;
-  }
-  if(prep_special_keys(ni)){
-    input_free_esctrie(&ni->inputescapes);
-    return -1;
-  }
-  return 0;
-}
-
-int ncinputlayer_init(tinfo* tcache, FILE* infp, queried_terminals_e* detected,
-                      unsigned* appsync, int* cursor_y, int* cursor_x,
-                      ncsharedstats* stats, unsigned* kittygraphs){
-  ncinputlayer* nilayer = &tcache->input;
-  if(pthread_mutex_init(&nilayer->lock, NULL)){
-    return -1;
-  }
-  nilayer->stats = stats;
-  nilayer->inputescapes = NULL;
-  nilayer->infd = fileno(infp);
-  loginfo("input fd: %d\n", nilayer->infd);
-  nilayer->ttyfd = tty_check(nilayer->infd) ? -1 : get_tty_fd(infp);
-  if(prep_all_keys(nilayer)){
-    pthread_mutex_destroy(&nilayer->lock);
-    return -1;
-  }
-  nilayer->inputbuf_occupied = 0;
-  nilayer->inputbuf_valid_starts = 0;
-  nilayer->inputbuf_write_at = 0;
-  nilayer->input_events = 0;
-  nilayer->creport_queue = NULL;
-  nilayer->user_wants_data = false;
-  nilayer->inner_wants_data = false;
-  pthread_cond_init(&nilayer->creport_cond, NULL);
-  int csifd = nilayer->ttyfd >= 0 ? nilayer->ttyfd : nilayer->infd;
-  if(tty_check(csifd)){
-    query_state inits = {
-      .tcache = tcache,
-      .state = STATE_NULL,
-      .qterm = *detected,
-      .cursor_x = cursor_x ? *cursor_x + 1 : -1,
-      .cursor_y = cursor_y ? *cursor_y + 1 : -1,
-    };
-    if(control_read(csifd, &inits)){
-      input_free_esctrie(&nilayer->inputescapes);
-      free(inits.version);
-      pthread_cond_destroy(&nilayer->creport_cond);
-      pthread_mutex_destroy(&nilayer->lock);
-      return -1;
-    }
-    tcache->bg_collides_default = inits.bg;
-    tcache->termversion = inits.version;
-    *detected = inits.qterm;
-    *appsync = inits.appsync;
-    if(cursor_x){
-      *cursor_x = inits.cursor_x - 1;
-    }
-    if(cursor_y){
-      *cursor_y = inits.cursor_y - 1;
-    }
-    if(inits.dimy && inits.dimx){
-      tcache->default_rows = inits.dimy;
-      tcache->default_cols = inits.dimx;
-    }
-    if(inits.pixelwidth && inits.pixelheight){
-      tcache->pixy = inits.pixelheight;
-      tcache->pixx = inits.pixelwidth;
-      if(tcache->default_rows && tcache->default_cols){
-        tcache->cellpixx = tcache->pixx / tcache->default_cols;
-        tcache->cellpixy = tcache->pixy / tcache->default_rows;
-      }
-    }
-    if(inits.kittygraphics){ // kitty trumps sixel
-      loginfo("advertised kitty; disabling sixel\n");
-      tcache->color_registers = 0;
-      tcache->sixel_maxx = 0;
-      tcache->sixel_maxy = 0;
-      *kittygraphs = true;
-    }
-  }
-  return 0;
-}
-
-// go through the accumulated input and find any cursor location reports.
-// enqueue them, and zero them out from the buffer (but do not decrement the
-// number of available characters).
-// FIXME optimize this via remembered offset + invalidation
-static void
-scan_for_clrs(ncinputlayer* ni){
-  unsigned pos = ni->inputbuf_valid_starts;
-  //unsigned count = ni->inputbuf_occupied;
-  // FIXME need handle_csi to work from an arbitrary place
-  if(ni->inputbuf[pos] == '\x1b'){
-    logdebug("Got the escape at %u\n", pos);
-    if(++pos == sizeof(ni->inputbuf) / sizeof(*ni->inputbuf)){
-      pos = 0;
-    }
-    logdebug("Got %c (%d) %u\n", ni->inputbuf[pos], ni->inputbuf[pos], pos);
-    if(ni->inputbuf[pos] == '['){
-      logdebug("Got the CSI at %u\n", pos);
-      pop_input_keypress(ni);
-      pop_input_keypress(ni);
-      ncinput nin;
-      // FIXME need real margins here, no?
-      if(handle_csi(ni, &nin, 0, 0) == NCKEY_CURSOR_LOCATION_REPORT){
-        enqueue_cursor_report(ni, &nin);
-      }
-    }
-  }
-}
-
-// assuming the user context is not active, go through current data looking
-// for a cursor location report. if we find none, block on input, and read if
-// appropriate. we can be interrupted by a new user context. we enter holding
-// the input lock, and leave holding the input lock, giving it up only while
-// blocking for readable action.
-void ncinput_extract_clrs(tinfo* ti){
-  ncinputlayer* ni = &ti->input;
-  do{
-    // FIXME doesn't this need locking?
-    if(ni->inputbuf_occupied){
-      scan_for_clrs(ni);
-      if(ni->creport_queue){
-        logdebug("Found a CLR, returning\n");
-        return;
-      }
-      logdebug("No CLR available, reading\n");
-    }
-    size_t rlen = input_queue_space(ni);
-    if(rlen){
-      if(rlen >= sizeof(ni->inputbuf) / sizeof(*ni->inputbuf) - ni->inputbuf_write_at){
-        rlen = sizeof(ni->inputbuf) / sizeof(*ni->inputbuf) - ni->inputbuf_write_at;
-      }
-      logdebug("Reading %llu from %d\n", rlen, ni->infd);
-      ssize_t r;
-      if((r = read(ni->infd, ni->inputbuf + ni->inputbuf_write_at, rlen)) > 0){
-        logdebug("Read %llu from %d\n", r, ni->infd);
-        ni->inputbuf_write_at += r;
-        if(ni->inputbuf_write_at == sizeof(ni->inputbuf) / sizeof(*ni->inputbuf)){
-          ni->inputbuf_write_at = 0;
-        }
-        ni->inputbuf_occupied += r;
-        continue;
-      }else if(r < 0){
-        ++ni->stats->s.input_errors;
-      }
-      ni->inner_wants_data = true;
-      pthread_mutex_unlock(&ni->lock);
-      // specify a NULL timeout, meaning we block as long as we need, until
-      // there's input available, or we are interrupted by a signal.
-      logdebug("Blocking on input");
-      if(block_on_input(ti, NULL) < 1){
-        pthread_mutex_lock(&ni->lock); // interrupted?
-        break;
-      }
-      ni->inner_wants_data = false;
-      logdebug("Reacquiring input lock");
-      pthread_mutex_lock(&ni->lock);
-    }
-  }while(!ni->user_wants_data);
-}
diff --git a/src/lib/input.h b/src/lib/input.h
deleted file mode 100644
index 24fbbd5f09..0000000000
--- a/src/lib/input.h
+++ /dev/null
@@ -1,69 +0,0 @@
-#ifndef NOTCURSES_INPUT
-#define NOTCURSES_INPUT
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-// internal header, not installed
-
-#include <stdio.h>
-
-struct tinfo;
-struct termios;
-struct ncinputlayer;
-struct ncsharedstats;
-
-typedef enum {
-    TERMINAL_UNKNOWN,       // no useful information from queries; use termname
-    // the very limited linux VGA/serial console, or possibly the (deprecated,
-    // pixel-drawable, RGBA8888) linux framebuffer console. *not* fbterm.
-    TERMINAL_LINUX,         // ioctl()s
-    // the linux KMS/DRM console, *not* kmscon, but DRM direct dumb buffers
-    TERMINAL_LINUXDRM,      // ioctl()s
-    TERMINAL_XTERM,         // XTVERSION == 'XTerm(ver)'
-    TERMINAL_VTE,           // TDA: "~VTE"
-    TERMINAL_KITTY,         // XTGETTCAP['TN'] == 'xterm-kitty'
-    TERMINAL_FOOT,          // TDA: "\EP!|464f4f54\E\\"
-    TERMINAL_MLTERM,        // XTGETTCAP['TN'] == 'mlterm'
-    TERMINAL_TMUX,          // XTVERSION == "tmux ver"
-    TERMINAL_WEZTERM,       // XTVERSION == 'WezTerm *'
-    TERMINAL_ALACRITTY,     // can't be detected; match TERM+DA2
-    TERMINAL_CONTOUR,       // XTVERSION == 'contour ver'
-    TERMINAL_ITERM,         // XTVERSION == 'iTerm2 [ver]'
-    TERMINAL_TERMINOLOGY,   // TDA: "~~TY"
-    TERMINAL_APPLE,         // Terminal.App, determined by TERM_PROGRAM + macOS
-    TERMINAL_MSTERMINAL,    // Microsoft Windows Terminal
-    TERMINAL_MINTTY,        // XTVERSION == 'mintty ver' MinTTY (Cygwin, MSYS2)
-} queried_terminals_e;
-
-// sets up the input layer, building a trie of escape sequences and their
-// nckey equivalents. if we are connected to a tty, this also completes the
-// terminal detection sequence (we ought have already written our initial
-// queries, ideally as early as possible). if we are able to determine the
-// terminal conclusively, it will be written to |detected|. if the terminal
-// advertised support for application-sychronized updates, |appsync| will be
-// non-zero.
-int ncinputlayer_init(struct tinfo* tcache, FILE* infp,
-                      queried_terminals_e* detected, unsigned* appsync,
-                      int* cursor_y, int* cursor_x,
-                      struct ncsharedstats* stats,
-                      unsigned* kittygraphs);
-
-void ncinputlayer_stop(struct ncinputlayer* nilayer);
-
-// FIXME absorb into ncinputlayer_init()
-int cbreak_mode(struct tinfo* ti);
-
-// assuming the user context is not active, go through current data looking
-// for a cursor location report. if we find none, block on input, and read if
-// appropriate. we can be interrupted by a new user context.
-void ncinput_extract_clrs(struct tinfo* ti);
-
-int ncinput_shovel(struct ncinputlayer* ni, const char* buf, size_t len);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
diff --git a/src/lib/internal.h b/src/lib/internal.h
index aebd3b8062..cf04c3f6e1 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -9,6 +9,7 @@ extern "C" {
 #include "builddef.h"
 #include "compat/compat.h"
 #include "notcurses/notcurses.h"
+#include "notcurses/direct.h"
 
 // KEY_EVENT is defined by both ncurses.h and wincon.h. since we don't use
 // either definition, kill it before inclusion of ncurses.h.
@@ -271,6 +272,15 @@ typedef struct nctabbed {
   nctabbed_options opts; // copied in nctabbed_create()
 } nctabbed;
 
+// various moving parts within a notcurses context (and the user) might need to
+// access the stats object, so throw a lock on it. we don't want the lock in
+// the actual structure since (a) it's usually unnecessary and (b) it breaks
+// memset() and memcpy().
+typedef struct ncsharedstats {
+  pthread_mutex_t lock;
+  ncstats s;
+} ncsharedstats;
+
 typedef struct ncdirect {
   ncpalette palette;         // 256-indexed palette can be used instead of/with RGB
   FILE* ttyfp;               // FILE* for output tty
@@ -279,6 +289,7 @@ typedef struct ncdirect {
   uint16_t stylemask;        // current styles
   bool initialized_readline; // have we initialized Readline?
   uint64_t flags;            // copied in ncdirect_init() from param
+  ncsharedstats stats;       // stats! not as broadly used as in notcurses
 } ncdirect;
 
 // Extracellular state for a cell during the render process. There is one
@@ -329,15 +340,6 @@ typedef struct ncpile {
   sprixel* sprixelcache;      // list of sprixels
 } ncpile;
 
-// various moving parts within a notcurses context (and the user) might need to
-// access the stats object, so throw a lock on it. we don't want the lock in
-// the actual structure since (a) it's usually unnecessary and (b) it breaks
-// memset() and memcpy().
-typedef struct ncsharedstats {
-  pthread_mutex_t lock;
-  ncstats s;
-} ncsharedstats;
-
 // the standard pile can be reached through ->stdplane.
 typedef struct notcurses {
   ncplane* stdplane; // standard plane, covers screen
@@ -375,7 +377,6 @@ typedef struct notcurses {
   int loglevel;
   ncpalette palette; // 256-indexed palette can be used instead of/with RGB
   bool palette_damage[NCPALETTESIZE];
-  unsigned stdio_blocking_save; // was stdio blocking at entry? restore on stop.
   uint64_t flags;  // copied from notcurses_options
 } notcurses;
 
@@ -1745,6 +1746,44 @@ tty_check(int fd){
   return isatty(fd);
 }
 
+// attempt to cancel the specified thread (not an error if we can't; it might
+// have already exited), and then join it (an error here is propagated).
+static inline int
+cancel_and_join(const char* name, pthread_t tid, void** res){
+  if(pthread_cancel(tid)){
+    logerror("couldn't cancel %s thread\n", name); // tid might have died
+  }
+  if(pthread_join(tid, res)){
+    logerror("error joining %s thread\n", name);
+    return -1;
+  }
+  return 0;
+}
+
+static inline int
+emit_scrolls(const tinfo* ti, int count, fbuf* f){
+  if(count > 1){
+    const char* indn = get_escape(ti, ESCAPE_INDN);
+    if(indn){
+      if(fbuf_emit(f, tiparm(indn, count)) < 0){
+        return -1;
+      }
+      return 0;
+    }
+  }
+  const char* ind = get_escape(ti, ESCAPE_IND);
+  if(ind == NULL){
+    ind = "\v";
+  }
+  while(count > 0){
+    if(fbuf_emit(f, ind) < 0){
+      return -1;
+    }
+    --count;
+  }
+  return 0;
+}
+
 #undef ALLOC
 #undef API
 
diff --git a/src/lib/linux.c b/src/lib/linux.c
index d93d134761..17fecf4e13 100644
--- a/src/lib/linux.c
+++ b/src/lib/linux.c
@@ -406,7 +406,7 @@ program_line_drawing_chars(int fd, struct unimapdesc* map){
     return 0;
   }
   if(ioctl(fd, PIO_UNIMAP, map)){
-    logwarn("Error setting kernel unicode map (%s)\n", strerror(errno));
+    logwarn("error setting kernel unicode map (%s)\n", strerror(errno));
     return -1;
   }
   loginfo("Successfully added %d kernel unicode mapping%s\n",
@@ -533,12 +533,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo,
         }
       }
       if(candidate == 0){
-        logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)half[s].w);
+        logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)half[s].w);
         // FIXME maybe don't want to error out here?
         return -1;
       }
       if(shim_quad_block(cfo, candidate, half[s].qbits)){
-        logwarn("Error replacing glyph for U+%04lx at %u\n", (long)half[s].w, candidate);
+        logwarn("error replacing glyph for U+%04lx at %u\n", (long)half[s].w, candidate);
         return -1;
       }
       if(add_to_map(map, half[s].w, candidate)){
@@ -555,12 +555,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo,
         }
       }
       if(candidate == 0){
-        logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)quads[s].w);
+        logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)quads[s].w);
         // FIXME maybe don't want to error out here?
         return -1;
       }
       if(shim_quad_block(cfo, candidate, quads[s].qbits)){
-        logwarn("Error replacing glyph for U+%04lx at %u\n", (long)quads[s].w, candidate);
+        logwarn("error replacing glyph for U+%04lx at %u\n", (long)quads[s].w, candidate);
         return -1;
       }
       if(add_to_map(map, quads[s].w, candidate)){
@@ -577,11 +577,11 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo,
         }
       }
       if(candidate == 0){
-        logwarn("Ran out of replaceable glyphs for U+%04lx\n", (long)eighths[s].w);
+        logwarn("ran out of replaceable glyphs for U+%04lx\n", (long)eighths[s].w);
         return -1;
       }
       if(shim_lower_eighths(cfo, candidate, eighths[s].qbits)){
-        logwarn("Error replacing glyph for U+%04lx at %u\n", (long)eighths[s].w, candidate);
+        logwarn("error replacing glyph for U+%04lx at %u\n", (long)eighths[s].w, candidate);
         return -1;
       }
       if(add_to_map(map, eighths[s].w, candidate)){
@@ -600,12 +600,12 @@ program_block_drawing_chars(tinfo* ti, int fd, struct console_font_op* cfo,
   }
   cfo->op = KD_FONT_OP_SET;
   if(ioctl(fd, KDFONTOP, cfo)){
-    logwarn("Error programming kernel font (%s)\n", strerror(errno));
+    logwarn("error programming kernel font (%s)\n", strerror(errno));
     kill_fbcopy(&fbdup);
     return -1;
   }
   if(ioctl(fd, PIO_UNIMAP, map)){
-    logwarn("Error setting kernel unicode map (%s)\n", strerror(errno));
+    logwarn("error setting kernel unicode map (%s)\n", strerror(errno));
     kill_fbcopy(&fbdup);
     return -1;
   }
@@ -636,7 +636,7 @@ reprogram_linux_font(tinfo* ti, int fd, struct console_font_op* cfo,
                      struct unimapdesc* map, unsigned no_font_changes,
                      bool* halfblocks, bool* quadrants){
   if(ioctl(fd, KDFONTOP, cfo)){
-    logwarn("Error reading Linux kernelfont (%s)\n", strerror(errno));
+    logwarn("error reading Linux kernelfont (%s)\n", strerror(errno));
     return -1;
   }
   loginfo("Kernel font size (glyphcount): %hu\n", cfo->charcount);
@@ -646,7 +646,7 @@ reprogram_linux_font(tinfo* ti, int fd, struct console_font_op* cfo,
     return -1;
   }
   if(ioctl(fd, GIO_UNIMAP, map)){
-    logwarn("Error reading Linux unimap (%s)\n", strerror(errno));
+    logwarn("error reading Linux unimap (%s)\n", strerror(errno));
     return -1;
   }
   loginfo("Kernel Unimap size: %hu/%hu\n", map->entry_ct, USHRT_MAX);
@@ -676,7 +676,7 @@ int reprogram_console_font(tinfo* ti, unsigned no_font_changes,
   size_t totsize = 128 * cfo.charcount; // FIXME enough?
   cfo.data = malloc(totsize);
   if(cfo.data == NULL){
-    logwarn("Error acquiring %zub for font descriptors (%s)\n", totsize, strerror(errno));
+    logwarn("error acquiring %zub for font descriptors (%s)\n", totsize, strerror(errno));
     return -1;
   }
   struct unimapdesc map = {};
@@ -684,7 +684,7 @@ int reprogram_console_font(tinfo* ti, unsigned no_font_changes,
   totsize = map.entry_ct * sizeof(struct unipair);
   map.entries = malloc(totsize);
   if(map.entries == NULL){
-    logwarn("Error acquiring %zub for Unicode font map (%s)\n", totsize, strerror(errno));
+    logwarn("error acquiring %zub for Unicode font map (%s)\n", totsize, strerror(errno));
     free(cfo.data);
     return -1;
   }
@@ -704,10 +704,10 @@ bool is_linux_console(int fd){
   }
   int mode;
   if(ioctl(fd, KDGETMODE, &mode)){
-    logdebug("Not a Linux console, KDGETMODE failed\n");
+    logdebug("not a Linux console, KDGETMODE failed\n");
     return false;
   }
-  loginfo("Verified Linux console, mode %d\n", mode);
+  loginfo("verified Linux console, mode %d\n", mode);
   return true;
 }
 
@@ -721,7 +721,7 @@ int get_linux_fb_pixelgeom(tinfo* ti, unsigned* ypix, unsigned *xpix){
   }
   struct fb_var_screeninfo fbi = {};
   if(ioctl(ti->linux_fb_fd, FBIOGET_VSCREENINFO, &fbi)){
-    logwarn("No framebuffer info from %s (%s?)\n", ti->linux_fb_dev, strerror(errno));
+    logwarn("no framebuffer info from %s (%s?)\n", ti->linux_fb_dev, strerror(errno));
     return -1;
   }
   loginfo("Linux %s geometry: %dx%d\n", ti->linux_fb_dev, fbi.yres, fbi.xres);
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 142cdd7c7e..f968d8dc64 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -1,4 +1,3 @@
-#include "input.h"
 #include "linux.h"
 #include "version.h"
 #include "egcpool.h"
@@ -107,10 +106,8 @@ notcurses_stop_minimal(void* vnc){
     if(nc->tcache.tpreserved){
       ret |= tcsetattr(nc->tcache.ttyfd, TCSAFLUSH, nc->tcache.tpreserved);
     }
-    if(nc->tcache.kittykbd){
-      if(tty_emit("\x1b[<u", nc->tcache.ttyfd)){
-        ret = -1;
-      }
+    if(tty_emit("\x1b[<u", nc->tcache.ttyfd)){
+      ret = -1;
     }
     if((esc = get_escape(&nc->tcache, ESCAPE_RMCUP))){
       if(sprite_clear_all(&nc->tcache, f)){ // send this to f
@@ -1000,7 +997,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     fprintf(stderr, "Provided an illegal negative margin, refusing to start\n");
     return NULL;
   }
-  if(opts->flags >= (NCOPTION_NO_FONT_CHANGES << 1u)){
+  if(opts->flags >= (NCOPTION_DRAIN_INPUT << 1u)){
     fprintf(stderr, "Warning: unknown Notcurses options %016" PRIu64 "\n", opts->flags);
   }
   notcurses* ret = malloc(sizeof(*ret));
@@ -1092,7 +1089,9 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
   if(interrogate_terminfo(&ret->tcache, opts->termtype, ret->ttyfp, utf8,
                           opts->flags & NCOPTION_NO_ALTERNATE_SCREEN, 0,
                           opts->flags & NCOPTION_NO_FONT_CHANGES,
-                          cursory, cursorx, &ret->stats)){
+                          cursory, cursorx, &ret->stats,
+                          ret->margin_l, ret->margin_t,
+                          opts->flags & NCOPTION_DRAIN_INPUT)){
     fbuf_free(&ret->rstate.f);
     pthread_mutex_destroy(&ret->pilelock);
     pthread_mutex_destroy(&ret->stats.lock);
@@ -1149,9 +1148,6 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
       ncplane_cursor_move_yx(ret->stdplane, ret->rstate.logendy, ret->rstate.logendx);
     }
   }
-  if(set_fd_nonblocking(ret->tcache.input.infd, 1, &ret->stdio_blocking_save)){
-    goto err;
-  }
   if(!(opts->flags & NCOPTION_NO_ALTERNATE_SCREEN)){
     // perform an explicit clear since the alternate screen was requested
     // (smcup *might* clear, but who knows? and it might not have been
@@ -1246,7 +1242,6 @@ int notcurses_stop(notcurses* nc){
       goto_location(nc, &nc->rstate.f, targy, 0);
       fbuf_finalize(&nc->rstate.f, stdout);
     }
-    ret |= set_fd_nonblocking(nc->tcache.input.infd, nc->stdio_blocking_save, NULL);
     if(nc->stdplane){
       notcurses_drop_planes(nc);
       free_plane(nc->stdplane);
@@ -2678,11 +2673,11 @@ int notcurses_lex_margins(const char* op, notcurses_options* opts){
 }
 
 int notcurses_inputready_fd(notcurses* n){
-  return n->tcache.input.infd;
+  return inputready_fd(n->tcache.ictx);
 }
 
 int ncdirect_inputready_fd(ncdirect* n){
-  return n->tcache.input.infd;
+  return inputready_fd(n->tcache.ictx);
 }
 
 // FIXME speed this up, PoC
diff --git a/src/lib/render.c b/src/lib/render.c
index ea1dd627c2..19b15f60e9 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -988,26 +988,7 @@ rasterize_scrolls(const ncpile* p, fbuf* f){
       return -1;
     }
   }
-  if(scrolls > 1){
-    const char* indn = get_escape(&p->nc->tcache, ESCAPE_INDN);
-    if(indn){
-      if(fbuf_emit(f, tiparm(indn, scrolls)) < 0){
-        return -1;
-      }
-      return 0;
-    }
-  }
-  const char* ind = get_escape(&p->nc->tcache, ESCAPE_IND);
-  if(ind == NULL){
-    ind = "\v";
-  }
-  while(scrolls > 0){
-    if(fbuf_emit(f, ind) < 0){
-      return -1;
-    }
-    --scrolls;
-  }
-  return 0;
+  return emit_scrolls(&p->nc->tcache, scrolls, f);
 }
 
 // second sprixel pass in rasterization. by this time, all sixels are handled
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index d70164e5e6..3399b805b5 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -5,7 +5,6 @@
 #endif
 #include "internal.h"
 #include "windows.h"
-#include "input.h"
 #include "linux.h"
 
 // there does not exist any true standard terminal size. with that said, we
@@ -167,7 +166,7 @@ match_termname(const char* termname, queried_terminals_e* qterm){
 }
 
 void free_terminfo_cache(tinfo* ti){
-  ncinputlayer_stop(&ti->input);
+  stop_inputlayer(ti);
   free(ti->termversion);
   free(ti->esctable);
 #ifdef __linux__
@@ -720,7 +719,8 @@ macos_early_matches(void){
 // full round trip before getting the reply, which is likely to pace init.
 int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned utf8,
                          unsigned noaltscreen, unsigned nocbreak, unsigned nonewfonts,
-                         int* cursor_y, int* cursor_x, ncsharedstats* stats){
+                         int* cursor_y, int* cursor_x, ncsharedstats* stats,
+                         int lmargin, int tmargin, unsigned draininput){
   int foolcursor_x, foolcursor_y;
   if(!cursor_x){
     cursor_x = &foolcursor_x;
@@ -790,6 +790,9 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut
   }
   tname = termname(); // longname() is also available
 #endif
+  if(init_inputlayer(ti, stdin, lmargin, tmargin, stats, draininput)){
+    goto err;
+  }
   ti->sprixel_scale_height = 1;
   get_default_geometry(ti);
   ti->caps.utf8 = utf8;
@@ -916,16 +919,52 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut
       goto err;
     }
   }
-  unsigned appsync_advertised = 0;
-  unsigned kittygraphs = 0;
-  if(ncinputlayer_init(ti, stdin, &ti->qterm, &appsync_advertised,
-                       cursor_y, cursor_x, stats, &kittygraphs)){
-    goto err;
+  unsigned kitty_graphics = 0;
+  if(ti->ttyfd >= 0){
+    struct initial_responses* iresp;
+    if((iresp = inputlayer_get_responses(ti->ictx)) == NULL){
+      goto err;
+    }
+    if(iresp->appsync_supported){
+      if(add_appsync_escapes_sm(ti, &tablelen, &tableused)){
+        free(iresp->version);
+        free(iresp);
+        goto err;
+      }
+    }
+    if(iresp->qterm != TERMINAL_UNKNOWN){
+      ti->qterm = iresp->qterm;
+    }
+    *cursor_y = iresp->cursory;
+    *cursor_x = iresp->cursorx;
+    ti->termversion = iresp->version;
+    if(iresp->dimy && iresp->dimx){
+      // FIXME probably oughtn't be setting the defaults, as this is just some
+      // random transient measurement?
+      ti->default_rows = iresp->dimy;
+      ti->default_cols = iresp->dimx;
+    }
+    if(iresp->pixy && iresp->pixx){
+      ti->pixy = iresp->pixy;
+      ti->pixx = iresp->pixx;
+    }
+    if(ti->default_rows && ti->default_cols){
+      ti->cellpixy = ti->pixy / ti->default_rows;
+      ti->cellpixx = ti->pixx / ti->default_cols;
+    }
+    ti->bg_collides_default = iresp->bg;
+    // kitty trumps sixel, when both are available
+    if((kitty_graphics = iresp->kitty_graphics) == 0){
+      ti->color_registers = iresp->color_registers;
+      ti->sixel_maxy = iresp->sixely;
+      ti->sixel_maxx = iresp->sixelx;
+    }
+    free(iresp);
   }
   if(nocbreak){
     if(ti->ttyfd >= 0){
+      // FIXME do this in input later, upon signaling completion?
       if(tcsetattr(ti->ttyfd, TCSANOW, ti->tpreserved)){
-        ncinputlayer_stop(&ti->input);
         goto err;
       }
     }
@@ -935,20 +974,14 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut
       goto err;
     }
   }
-  if(appsync_advertised){
-    if(add_appsync_escapes_sm(ti, &tablelen, &tableused)){
-      goto err;
-    }
-  }
   bool invertsixel = false;
   if(apply_term_heuristics(ti, tname, ti->qterm, &tablelen, &tableused,
                            &invertsixel, nonewfonts)){
-    ncinputlayer_stop(&ti->input);
     goto err;
   }
   build_supported_styles(ti);
   if(ti->pixel_draw == NULL && ti->pixel_draw_late == NULL){
-    if(kittygraphs){
+    if(kitty_graphics){
       setup_kitty_bitmaps(ti, ti->ttyfd, NCPIXEL_KITTY_ANIMATED);
     }
     // our current sixel quantization algorithm requires at least 64 color
@@ -967,6 +1000,7 @@ int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out, unsigned ut
     free(ti->tpreserved);
     ti->tpreserved = NULL;
   }
+  stop_inputlayer(ti);
   free(ti->esctable);
   free(ti->termversion);
   del_curterm(cur_term);
@@ -992,48 +1026,6 @@ char* termdesc_longterm(const tinfo* ti){
   return ret;
 }
 
-// when we have input->ttyfd, everything's simple -- we're reading from a
-// different source than the user is, so we can just write the query, and block
-// on the response, easy peasy.
-// FIXME still, we ought reuse buffer, and pass on any excess reads...
-static int
-locate_cursor_simple(tinfo* ti, const char* u7, int* cursor_y, int* cursor_x){
-  if(ti->qterm == TERMINAL_MSTERMINAL){
-    return locate_cursor(ti, cursor_y, cursor_x);
-  }
-  char* buf = malloc(BUFSIZ);
-  if(buf == NULL){
-    return -1;
-  }
-  loginfo("sending cursor report request\n");
-  if(tty_emit(u7, ti->ttyfd)){
-    free(buf);
-    return -1;
-  }
-  ssize_t r;
-  do{
-    if((r = read(ti->input.infd, buf, BUFSIZ - 1)) > 0){
-      buf[r] = '\0';
-      if(sscanf(buf, "\e[%d;%dR", cursor_y, cursor_x) != 2){
-        loginfo("not a cursor location report: %s\n", buf);
-        free(buf);
-        return -1;
-      }
-      --*cursor_y;
-      --*cursor_x;
-      break;
-    }
-  }while(errno == EAGAIN || errno == EWOULDBLOCK || errno == EBUSY || errno == EINTR);
-  if(r < 0){
-    logerror("error reading cursor location from %d (%s)\n", ti->input.infd, strerror(errno));
-    free(buf);
-    return -1;
-  }
-  free(buf);
-  loginfo("located cursor with %d: %d/%d\n", ti->ttyfd, *cursor_y, *cursor_x);
-  return 0;
-}
-
 // send a u7 request, and wait until we have a cursor report. if input's ttyfd
 // is valid, we can just camp there. otherwise, we need dance with potential
 // user input looking at infd.
@@ -1058,47 +1050,54 @@ int locate_cursor(tinfo* ti, int* cursor_y, int* cursor_x){
     logwarn("No support in terminfo\n");
     return -1;
   }
-  if(ti->ttyfd >= 0){
-    return locate_cursor_simple(ti, u7, cursor_y, cursor_x);
-  }
-  int fd = ti->input.infd;
-  if(fd < 0){
+  if(ti->ttyfd < 0){
     logwarn("No valid path for cursor report\n");
     return -1;
   }
-  bool emitted_u7 = false; // only want to send one max
-  cursorreport* clr;
-  loginfo("Acquiring input lock\n");
-  pthread_mutex_lock(&ti->input.lock);
-  while((clr = ti->input.creport_queue) == NULL){
-    logdebug("No report yet\n");
-    if(!emitted_u7){
-      logdebug("Emitting u7\n");
-      // FIXME i'd rather not do this while holding the lock =[
-      if(tty_emit(u7, fd)){
-        pthread_mutex_unlock(&ti->input.lock);
-        return -1;
-      }
-      emitted_u7 = true;
-    }
-    // this can block. we must enter holding the input lock, and it will
-    // return to us holding the input lock.
-    ncinput_extract_clrs(ti);
-    if( (clr = ti->input.creport_queue) ){
-      break;
-    }
-    pthread_cond_wait(&ti->input.creport_cond, &ti->input.lock);
-  }
-  ti->input.creport_queue = clr->next;
-  pthread_mutex_unlock(&ti->input.lock);
-  loginfo("Got a report from %d %d/%d\n", fd, clr->y, clr->x);
-  *cursor_y = clr->y;
-  *cursor_x = clr->x;
-  if(ti->inverted_cursor){
-    int tmp = *cursor_y;
-    *cursor_y = *cursor_x;
-    *cursor_x = tmp;
-  }
-  free(clr);
+  int fd = ti->ttyfd;
+  if(tty_emit(u7, fd)){
+    return -1;
+  }
+  get_cursor_location(ti->ictx, cursor_y, cursor_x);
+  loginfo("got a report from %d %d/%d\n", fd, *cursor_y, *cursor_x);
+  return 0;
+}
+
+int cbreak_mode(tinfo* ti){
+#ifndef __MINGW64__
+  int ttyfd = ti->ttyfd;
+  if(ttyfd < 0){
+    return 0;
+  }
+  // assume it's not a true terminal (e.g. we might be redirected to a file)
+  struct termios modtermios;
+  memcpy(&modtermios, ti->tpreserved, sizeof(modtermios));
+  // see termios(3). disabling ECHO and ICANON means input will not be echoed
+  // to the screen, input is made available without enter-based buffering, and
+  // line editing is disabled. since we have not gone into raw mode, ctrl+c
+  // etc. still have their typical effects. ICRNL maps return to 13 (Ctrl+M)
+  // instead of 10 (Ctrl+J).
+  modtermios.c_lflag &= (~ECHO & ~ICANON);
+  modtermios.c_iflag &= ~ICRNL;
+  if(tcsetattr(ttyfd, TCSANOW, &modtermios)){
+    logerror("Error disabling echo / canonical on %d (%s)\n", ttyfd, strerror(errno));
+    return -1;
+  }
+#else
+  // we don't yet have a way to take Cygwin/MSYS2 out of canonical mode. we'll
+  // hit this stanza in MSYS2; allow the GetConsoleMode() to fail for now. this
+  // means we'll need enter pressed after the query response, obviously an
+  // unacceptable state of affairs...FIXME
+  DWORD mode;
+  if(!GetConsoleMode(ti->inhandle, &mode)){
+    logerror("error acquiring input mode\n");
+    return 0; // FIXME is it safe?
+  }
+  mode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
+  if(!SetConsoleMode(ti->inhandle, mode)){
+    logerror("error setting input mode\n");
+    return -1;
+  }
+#endif
   return 0;
 }
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 04129097e8..33802e502d 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -9,12 +9,12 @@ extern "C" {
 
 #include "version.h"
 #include "builddef.h"
-#include "input.h"
 #include <stdint.h>
 #include <pthread.h>
 #include <stdbool.h>
 #include <notcurses/notcurses.h>
 #include "fbuf.h"
+#include "in.h"
 
 struct ncpile;
 struct sprixel;
@@ -88,43 +88,6 @@ typedef struct cursorreport {
   struct cursorreport* next;
 } cursorreport;
 
-// we read input from one or two places. if stdin is connected to our
-// controlling tty, we read only from that file descriptor. if it is
-// connected to something else, and we have a controlling tty, we will
-// read data only from stdin and control only from the tty. if we have
-// no connected tty, only data is available.
-typedef struct ncinputlayer {
-  // only allow one reader at a time, whether it's the user trying to do so,
-  // or our desire for a cursor report competing with the user.
-  pthread_mutex_t lock;
-  // must be held to operate on the cursor report queue shared between pure
-  // input and the control layer.
-  pthread_cond_t creport_cond;
-  // ttyfd is only valid if we are connected to a tty, *and* stdin is not
-  // connected to that tty (this usually means stdin was redirected). in that
-  // case, we read control sequences only from ttyfd.
-  int ttyfd; // file descriptor for connected tty
-  int infd;  // file descriptor for processing input, from stdin
-  unsigned char inputbuf[BUFSIZ];
-  unsigned char csibuf[BUFSIZ];   // running buffer while parsing CSIs
-  // we keep a wee ringbuffer of input queued up for delivery. if
-  // inputbuf_occupied == sizeof(inputbuf), there is no room. otherwise, data
-  // can be read to inputbuf_write_at until we fill up. the first datum
-  // available for the app is at inputbuf_valid_starts iff inputbuf_occupied is
-  // not 0. the main purpose is working around bad predictions of escapes.
-  unsigned inputbuf_occupied;
-  unsigned inputbuf_valid_starts;
-  unsigned inputbuf_write_at;
-  // number of input events seen. does not belong in ncstats, since it must not
-  // be reset (semantics are relied upon by widgets for mouse click detection).
-  uint64_t input_events;
-  struct esctrie* inputescapes; // trie of input escapes -> ncspecial_keys
-  cursorreport* creport_queue; // queue of cursor reports
-  bool user_wants_data;        // a user context is active
-  bool inner_wants_data;       // if we're blocking on input
-  struct ncsharedstats* stats; // notcurses sharedstats object
-} ncinputlayer;
-
 // terminal interface description. most of these are acquired from terminfo(5)
 // (using a database entry specified by TERM). some are determined via
 // heuristics based off terminal interrogation or the TERM environment
@@ -198,7 +161,8 @@ typedef struct tinfo {
   queried_terminals_e qterm; // detected terminal class
   // we heap-allocate this one (if we use it), as it's not fully defined on Windows
   struct termios *tpreserved;// terminal state upon entry
-  ncinputlayer input;        // input layer
+  struct inputctx* ictx;     // new input layer
+  unsigned stdio_blocking_save; // was stdio blocking at entry? restore on stop.
 
   // if we get a reply to our initial \e[18t cell geometry query, it will
   // replace these values. note that LINES/COLUMNS cannot be used to limit
@@ -206,8 +170,6 @@ typedef struct tinfo {
   int default_rows;          // LINES environment var / lines terminfo / 24
   int default_cols;          // COLUMNS environment var / cols terminfo / 80
 
-  unsigned kittykbd;         // kitty keyboard support level
-
   int gpmfd;                 // connection to GPM daemon
   pthread_t gpmthread;       // thread handle for GPM watcher
 #ifdef __linux__
@@ -220,10 +182,6 @@ typedef struct tinfo {
   HANDLE outhandle;
 #endif
 
-  // some terminals (e.g. kmscon) return cursor coordinates inverted from the
-  // typical order. we detect it the first time ncdirect_cursor_yx() is called.
-  bool detected_cursor_inversion; // have we performed inversion testing?
-  bool inverted_cursor;      // does the terminal return inverted coordinates?
   bool bce;                  // is the bce property advertised?
   bool in_alt_screen;        // are we in the alternate screen?
 } tinfo;
@@ -252,7 +210,9 @@ term_supported_styles(const tinfo* ti){
 int interrogate_terminfo(tinfo* ti, const char* termtype, FILE* out,
                          unsigned utf8, unsigned noaltscreen, unsigned nocbreak,
                          unsigned nonewfonts, int* cursor_y, int* cursor_x,
-                         struct ncsharedstats* stats);
+                         struct ncsharedstats* stats, int lmargin, int tmargin,
+                         unsigned draininput)
+  __attribute__ ((nonnull (1, 2, 3, 10)));
 
 void free_terminfo_cache(tinfo* ti);
 
@@ -382,6 +342,8 @@ leave_alternate_screen(FILE* fp, tinfo* ti){
   return 0;
 }
 
+int cbreak_mode(tinfo* ti);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/poc/bitmapstates.c b/src/poc/bitmapstates.c
index eef84d3bbb..12fbef756b 100644
--- a/src/poc/bitmapstates.c
+++ b/src/poc/bitmapstates.c
@@ -149,6 +149,7 @@ wipebitmap(struct notcurses* nc){
 int main(void){
   struct notcurses_options opts = {
     .loglevel = NCLOGLEVEL_TRACE,
+    .flags = NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(notcurses_check_pixel_support(nc) < 1){
diff --git a/src/poc/blitters.c b/src/poc/blitters.c
index 43b694303b..d8b7080f7f 100644
--- a/src/poc/blitters.c
+++ b/src/poc/blitters.c
@@ -14,7 +14,8 @@ int main(int argc, char** argv){
     return EXIT_FAILURE;
   }
   struct notcurses_options nopts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN,
+    .flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_init(&nopts, NULL);
   if(nc == NULL){
diff --git a/src/poc/cjkscroll.c b/src/poc/cjkscroll.c
index 691713ffb5..a93351727d 100644
--- a/src/poc/cjkscroll.c
+++ b/src/poc/cjkscroll.c
@@ -7,6 +7,7 @@ int main(void){
   notcurses_options nopts = {
     .flags = NCOPTION_INHIBIT_SETLOCALE
              | NCOPTION_NO_ALTERNATE_SCREEN
+             | NCOPTION_DRAIN_INPUT
              | NCOPTION_PRESERVE_CURSOR,
   };
   struct notcurses* nc = notcurses_core_init(&nopts, NULL);
diff --git a/src/poc/cursor.c b/src/poc/cursor.c
index 89f1fd6f90..efb93b4a74 100644
--- a/src/poc/cursor.c
+++ b/src/poc/cursor.c
@@ -29,7 +29,8 @@ int main(void){
   if(rendered_cursor()){
     return EXIT_FAILURE;
   }
-  uint64_t flags = NCDIRECT_OPTION_VERY_VERBOSE;
+  uint64_t flags = NCDIRECT_OPTION_VERY_VERBOSE
+                   | NCDIRECT_OPTION_DRAIN_INPUT;
   struct ncdirect* n = ncdirect_core_init(NULL, stdout, flags);
   if(n == NULL){
     return EXIT_FAILURE;
diff --git a/src/poc/dirgb.c b/src/poc/dirgb.c
index d47ec48e27..562f551010 100644
--- a/src/poc/dirgb.c
+++ b/src/poc/dirgb.c
@@ -52,7 +52,8 @@ int main(void){
   if(!setlocale(LC_ALL, "")){
     return EXIT_FAILURE;
   }
-  struct ncdirect* nc = ncdirect_core_init(NULL, stdout, 0);
+  uint64_t flags = NCDIRECT_OPTION_DRAIN_INPUT;
+  struct ncdirect* nc = ncdirect_core_init(NULL, stdout, flags);
   if(!nc){
     return EXIT_FAILURE;
   }
diff --git a/src/poc/dirlines.c b/src/poc/dirlines.c
index 2a97e2a992..b18f09a0ae 100644
--- a/src/poc/dirlines.c
+++ b/src/poc/dirlines.c
@@ -6,7 +6,8 @@ int main(void){
   if(!setlocale(LC_ALL, "")){
     return EXIT_FAILURE;
   }
-  struct ncdirect* n = ncdirect_core_init(NULL, stdout, 0);
+  uint64_t flags = NCDIRECT_OPTION_DRAIN_INPUT;
+  struct ncdirect* n = ncdirect_core_init(NULL, stdout, flags);
   putchar('\n');
   for(int i = 0 ; i < 15 ; ++i){
     uint64_t c1 = 0, c2 = 0;
diff --git a/src/poc/fbconscroll.c b/src/poc/fbconscroll.c
index fd4df5156c..47cb543c3a 100644
--- a/src/poc/fbconscroll.c
+++ b/src/poc/fbconscroll.c
@@ -18,7 +18,8 @@ int main(int argc, char** argv){
              | NCOPTION_NO_CLEAR_BITMAPS
              | NCOPTION_SUPPRESS_BANNERS
              | NCOPTION_NO_FONT_CHANGES
-             | NCOPTION_PRESERVE_CURSOR,
+             | NCOPTION_PRESERVE_CURSOR
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_init(&nopts, NULL);
   if(nc == NULL){
diff --git a/src/poc/fileroller.c b/src/poc/fileroller.c
index 24ee90113e..3defbb1227 100644
--- a/src/poc/fileroller.c
+++ b/src/poc/fileroller.c
@@ -43,8 +43,9 @@ int main(int argc, char** argv){
   }
   setlocale(LC_ALL, "");
   notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE |
-              NCOPTION_SUPPRESS_BANNERS,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+             | NCOPTION_SUPPRESS_BANNERS
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   struct ncplane* n = notcurses_stdplane(nc);
diff --git a/src/poc/gradients.c b/src/poc/gradients.c
index 9bedf67925..017d6d565c 100644
--- a/src/poc/gradients.c
+++ b/src/poc/gradients.c
@@ -70,7 +70,8 @@ int main(void){
     return EXIT_FAILURE;
   }
   struct notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(nc == NULL){
diff --git a/src/poc/interp.c b/src/poc/interp.c
index 91d12c59bb..c6ae2e350e 100644
--- a/src/poc/interp.c
+++ b/src/poc/interp.c
@@ -95,6 +95,7 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
 int main(void){
   struct notcurses_options nopts = {
 //    .loglevel = NCLOGLEVEL_TRACE,
+      .flags = NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_init(&nopts, NULL);
   if(nc == NULL){
diff --git a/src/poc/pixel.c b/src/poc/pixel.c
index 265d7a0aa1..ea2eca7546 100644
--- a/src/poc/pixel.c
+++ b/src/poc/pixel.c
@@ -49,6 +49,7 @@ int main(int argc, char** argv){
     .margin_l = 2,
     .margin_b = 2,
     .margin_r = 2,
+    .flags = NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_init(&opts, NULL);
   if(notcurses_check_pixel_support(nc) <= 0){
diff --git a/src/poc/procroller.c b/src/poc/procroller.c
index 5e9fc3e7e5..54b3d76d07 100644
--- a/src/poc/procroller.c
+++ b/src/poc/procroller.c
@@ -43,8 +43,9 @@ int main(int argc, char** argv){
   }
   setlocale(LC_ALL, "");
   notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE |
-              NCOPTION_SUPPRESS_BANNERS,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+              | NCOPTION_SUPPRESS_BANNERS
+              | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(nc == NULL){
diff --git a/src/poc/qrcode.c b/src/poc/qrcode.c
index 14fd2bc751..c6114e01a7 100644
--- a/src/poc/qrcode.c
+++ b/src/poc/qrcode.c
@@ -25,7 +25,9 @@ int main(int argc, const char** argv){
     return EXIT_FAILURE;
   }
   struct notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+             | NCOPTION_NO_ALTERNATE_SCREEN
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_init(&opts, NULL);
   int dimy, dimx;
diff --git a/src/poc/rgb.c b/src/poc/rgb.c
index 76ee26db2d..84a936a328 100644
--- a/src/poc/rgb.c
+++ b/src/poc/rgb.c
@@ -9,7 +9,9 @@ int main(void){
     return EXIT_FAILURE;
   }
   struct notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+             | NCOPTION_NO_ALTERNATE_SCREEN
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(nc == NULL){
diff --git a/src/poc/rotate.c b/src/poc/rotate.c
index 04a32be3af..ea1369c063 100644
--- a/src/poc/rotate.c
+++ b/src/poc/rotate.c
@@ -22,7 +22,9 @@ int main(int argc, char** argv){
   }
   notcurses_options opts = {};
   //opts.loglevel = NCLOGLEVEL_TRACE;
-  opts.flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN;
+  opts.flags = NCOPTION_INHIBIT_SETLOCALE
+                | NCOPTION_NO_ALTERNATE_SCREEN
+                | NCOPTION_DRAIN_INPUT;
   struct notcurses* nc;
   if((nc = notcurses_init(&opts, NULL)) == NULL){
     return EXIT_FAILURE;
diff --git a/src/poc/rtl.c b/src/poc/rtl.c
index 0315ae66d1..3f09d9a88d 100644
--- a/src/poc/rtl.c
+++ b/src/poc/rtl.c
@@ -3,7 +3,8 @@
 
 int main(void){
   struct notcurses_options opts = {
-    .flags = NCOPTION_NO_ALTERNATE_SCREEN,
+    .flags = NCOPTION_NO_ALTERNATE_SCREEN
+              | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(!nc){
diff --git a/src/poc/scroll.c b/src/poc/scroll.c
index 7601e94c5f..f803206f15 100644
--- a/src/poc/scroll.c
+++ b/src/poc/scroll.c
@@ -5,8 +5,10 @@
 int main(void){
   setlocale(LC_ALL, "");
   struct notcurses_options opts = {
-    .flags = NCOPTION_INHIBIT_SETLOCALE | NCOPTION_NO_ALTERNATE_SCREEN
-              | NCOPTION_PRESERVE_CURSOR,
+    .flags = NCOPTION_INHIBIT_SETLOCALE
+              | NCOPTION_NO_ALTERNATE_SCREEN
+              | NCOPTION_PRESERVE_CURSOR
+              | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&opts, NULL);
   if(nc == NULL){
diff --git a/src/poc/sgr-full.c b/src/poc/sgr-full.c
index 10cd82a1bd..44845b15c4 100644
--- a/src/poc/sgr-full.c
+++ b/src/poc/sgr-full.c
@@ -6,7 +6,8 @@ int main(void){
   struct notcurses_options nopts = {
     .flags = NCOPTION_NO_ALTERNATE_SCREEN
              | NCOPTION_SUPPRESS_BANNERS
-             | NCOPTION_PRESERVE_CURSOR,
+             | NCOPTION_PRESERVE_CURSOR
+             | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&nopts, NULL);
   if(nc == NULL){
diff --git a/src/poc/statepixel.c b/src/poc/statepixel.c
index e55b0be62f..4487926a03 100644
--- a/src/poc/statepixel.c
+++ b/src/poc/statepixel.c
@@ -115,7 +115,8 @@ int main(int argc, char **argv){
   }
   char** a = argv + 1;
   struct notcurses_options opts = {
-    .flags = NCOPTION_NO_ALTERNATE_SCREEN,
+    .flags = NCOPTION_NO_ALTERNATE_SCREEN
+              | NCOPTION_DRAIN_INPUT,
     //.loglevel = NCLOGLEVEL_TRACE,
   };
   struct notcurses* nc = notcurses_init(&opts, NULL);
diff --git a/src/poc/zalgo.c b/src/poc/zalgo.c
index e759d5bbd7..35d253e21f 100644
--- a/src/poc/zalgo.c
+++ b/src/poc/zalgo.c
@@ -3,7 +3,9 @@
 
 int main(void){
   struct notcurses_options nops = {
-    .flags = NCOPTION_NO_ALTERNATE_SCREEN | NCOPTION_SUPPRESS_BANNERS,
+    .flags = NCOPTION_NO_ALTERNATE_SCREEN
+              | NCOPTION_SUPPRESS_BANNERS
+              | NCOPTION_DRAIN_INPUT,
   };
   struct notcurses* nc = notcurses_core_init(&nops, NULL);
   if(nc == NULL){


Test patch:
diff --git a/src/tests/cell.cpp b/src/tests/cell.cpp
index f470585112..7fe450bec2 100644
--- a/src/tests/cell.cpp
+++ b/src/tests/cell.cpp
@@ -42,7 +42,7 @@ TEST_CASE("Cell") {
     CHECK(9 == nccell_load(n_, &c, "นี้"));
     WARN(1 == nccell_cols(&c));
 
-    // type-3 woman playing water polo, 17 bytes (5 characters)
+    // type-3 woman playing water polo, 17 bytes (5 characters, 2 columns)
 #ifdef __linux__
     CHECK(17 == nccell_load(n_, &c, "\U0001f93d\U0001f3fc\u200d\u2640\ufe0f"));
     WARN(2 == nccell_cols(&c));