Repository: dankamongmen/notcurses (language: python)

Issue:
need portable memstream abstraction to replace POSIX memstream
see comments in #463--windows doesn't have `open_memstream()`, and it doesn't look easy to fake one. let's do our own abstraction atop `mmap()`/`CreateFileMappingA()`. this ought offer several advantages:

(a) no `FILE*` atop it, eliminating the need to `fclose()`+`free()`; we can just do one destroy call
(b) huge pages!
(c) ought lead to fewer heavyweight `printf()` style outputs
need portable memstream abstraction to replace POSIX memstream
see comments in #463--windows doesn't have `open_memstream()`, and it doesn't look easy to fake one. let's do our own abstraction atop `mmap()`/`CreateFileMappingA()`. this ought offer several advantages:

(a) no `FILE*` atop it, eliminating the need to `fclose()`+`free()`; we can just do one destroy call
(b) huge pages!
(c) ought lead to fewer heavyweight `printf()` style outputs
need portable memstream abstraction to replace POSIX memstream
see comments in #463--windows doesn't have `open_memstream()`, and it doesn't look easy to fake one. let's do our own abstraction atop `mmap()`/`CreateFileMappingA()`. this ought offer several advantages:

(a) no `FILE*` atop it, eliminating the need to `fclose()`+`free()`; we can just do one destroy call
(b) huge pages!
(c) ought lead to fewer heavyweight `printf()` style outputs
need portable memstream abstraction to replace POSIX memstream
see comments in #463--windows doesn't have `open_memstream()`, and it doesn't look easy to fake one. let's do our own abstraction atop `mmap()`/`CreateFileMappingA()`. this ought offer several advantages:

(a) no `FILE*` atop it, eliminating the need to `fclose()`+`free()`; we can just do one destroy call
(b) huge pages!
(c) ought lead to fewer heavyweight `printf()` style outputs
complete implementation of iTerm2 protocol backend
We're not yet doing wipes nor restores on iTerm2 (see #1420), and probably won't be until we have answers to the questions raised in that issue regarding the intentions and implementations of that protocol (@gnachman will hopefully let us know).

One interesting thing I've noticed in at least the WezTerm implementation is that `ncplayer -bpixel -snone` appears to scale to the visual area size if the image is too large. That's unlike either Kitty's behavior or any Sixel implementation of which I am aware.

It kinda looks like iTerm2 mostly works like Sixel in terms of transparency and interaction with glyphs. If that's the case, we ought be able to take most of the invalidation/damage logic from Sixel, and just change the reencoding (though note what I said in that bug about PNG being a bitch and a half to change on the fly, what with base64 plus IDAT plus deflate plus scanlines).

we're already doing scrubbing via `sixel_scrub()`, which works for this just fine. we need to implement:

* [ ] `iterm_wipe()`
* [ ] `iterm_rebuild()`
need portable memstream abstraction to replace POSIX memstream
see comments in #463--windows doesn't have `open_memstream()`, and it doesn't look easy to fake one. let's do our own abstraction atop `mmap()`/`CreateFileMappingA()`. this ought offer several advantages:

(a) no `FILE*` atop it, eliminating the need to `fclose()`+`free()`; we can just do one destroy call
(b) huge pages!
(c) ought lead to fewer heavyweight `printf()` style outputs
pull terminfo code out of windows build
from what i can tell, terminfo isn't much of a thing on windows (@magiblot seconds this). i certainly haven't seen it work even through `setupterm()`. i think we ought just purge it from the windows build and assume windows terminal [shrug].
Hardcode Windows Terminal control sequences
Now that #2009 is complete, we're no longer failing in the `setupterm()` call on Windows. We're also entirely without control sequence data. Using the handy guide at https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences, hardcode these in a Windows-only variant of `interrogate_terminfo()`. I think we'll then work on Windows, in some capacity.
Hardcode Windows Terminal control sequences
Now that #2009 is complete, we're no longer failing in the `setupterm()` call on Windows. We're also entirely without control sequence data. Using the handy guide at https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences, hardcode these in a Windows-only variant of `interrogate_terminfo()`. I think we'll then work on Windows, in some capacity.
Hardcode Windows Terminal control sequences
Now that #2009 is complete, we're no longer failing in the `setupterm()` call on Windows. We're also entirely without control sequence data. Using the handy guide at https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences, hardcode these in a Windows-only variant of `interrogate_terminfo()`. I think we'll then work on Windows, in some capacity.


Discussion / hints:
hard dependency of #463 
hard dependency of #463 
hard dependency of #463 
hard dependency of #463 
On the plus side, with changes just now to properly terminate the 1337 OSC with ST, iTerm2 runs `ncls` flawlessly (at least in WezTerm).
hard dependency of #463 





Fix patch:
diff --git a/INSTALL.md b/INSTALL.md
index 3a91bb974d..8807bb48be 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -10,13 +10,13 @@ prepackaged on many distributions. Otherwise, acquire the current source via
 
 ### APT
 
-On an APT-based distribution, run:
+Install build dependencies:
 
 `apt-get install build-essential cmake doctest-dev zlib1g-dev libavformat-dev libavutil-dev libncurses-dev libreadline-dev libqrcodegen-dev libswscale-dev libunistring-dev pandoc pkg-config`
 
-If you only intend to build core Notcurses (without multimedia support), run:
-
-`apt-get install build-essential cmake doctest-dev zlib1g-dev libncurses-dev libreadline-dev libqrcodegen-dev libunistring-dev pandoc pkg-config`
+If you only intend to build core Notcurses (without multimedia support), you
+can omit `libavformat-dev`, `libavutil-dev`, and `libswscale-dev` from this
+list.
 
 If you want to build the Python wrappers, you'll also need:
 
@@ -28,8 +28,26 @@ If you want to build the Rust wrappers, you'll also need:
 
 ### RPM
 
+Install build dependencies:
+
 `dnf install cmake doctest-devel zlib-devel ncurses-devel readline-devel libqrcodegen-devel libunistring-devel OpenImageIO-devel pandoc`
 
+If you only intend to build core Notcurses (without multimedia support), you
+can omit `OpenImageIO-devel`. If you're building outside Fedora Core (e.g. with
+RPM Fusion), you might want to use FFmpeg rather than OpenImageIO.
+
+### Microsoft Windows
+
+Currently, building on Windows requires [MSYS2](https://www.msys2.org/) in its
+64-bit Universal C Runtime (UCRT) incarnation. This builds native Windows DLLs
+and EXEs, though it does not use Visual Studio. Install build dependencies:
+
+`pacman -S mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-doctest mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-libunistring mingw-w64-ucrt-x86_64-ncurses mingw-w64-ucrt-x86_64-rust mingw-w64-ucrt-x86_64-toolchain`
+
+If you only intend to build core Notcurses (without multimedia support), you
+can omit `mingw-w64-ucrt-x86_64-ffmpeg`. The Windows build never uses GNU
+Readline, so don't worry about that.
+
 ## Building
 
 * Create a subdirectory, traditionally `build` (this is not strictly necessary,
@@ -95,4 +113,5 @@ but must be `Debug` for use of `USE_COVERAGE`.
 * `USE_PANDOC`: build man pages with pandoc
 * `USE_POC`: build small, uninstalled proof-of-concept binaries
 * `USE_QRCODEGEN`: build qrcode support via libqrcodegen
+* `USE_READLINE`: build readline support for Direct Mode
 * `USE_STATIC`: build static libraries (in addition to shared ones)
diff --git a/include/notcurses/ncport.h b/include/notcurses/ncport.h
index fc38ca04dd..ec94187b2f 100644
--- a/include/notcurses/ncport.h
+++ b/include/notcurses/ncport.h
@@ -20,15 +20,14 @@ extern "C" {
 #include <libkern/OSByteOrder.h>
 #define htole(x) (OSSwapInt32(htonl(x)))
 #elif defined(__MINGW64__)
-#include <winsock2.h>
-#include <winsock.h>
-#define htole(x) (_byteswap_ulong(htonl(x)))
+#define htole(x) (x) // FIXME
 // FIXME placeholders, need real solutions here
 #define wcwidth(w) 1
 #define wcswidth(w, s) (s)
 #define sigset_t int
 #define sigemptyset(x)
 #define O_CLOEXEC O_NOINHERIT
+#define O_NONBLOCK 0
 #define O_DIRECTORY 0
 #define S_IFLNK 0
 #else // bsd
diff --git a/src/compat/compat.c b/src/compat/compat.c
index 8fa38ee125..572ed451ff 100644
--- a/src/compat/compat.c
+++ b/src/compat/compat.c
@@ -1,8 +1,10 @@
-#ifndef __linux__
-#ifndef __FreeBSD__
+#include "compat/compat.h"
 #ifdef  __MINGW64__
 #include <string.h>
 #include <stdlib.h>
+#include <synchapi.h>
+#include <handleapi.h>
+#include <processthreadsapi.h>
 char* strndup(const char* str, size_t size){
   size_t t = strlen(str);
   if(t > size){
@@ -16,10 +18,49 @@ char* strndup(const char* str, size_t size){
   }
   return r;
 }
-#else
+int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate){ // FIXME
+  (void)fd;
+  (void)state;
+  (void)oldstate;
+  return 0;
+}
+#define INFINITE 0xffffffff // FIXME
+pid_t waitpid(pid_t pid, int* state, int options){
+  (void)options; // FIXME honor WNOHANG
+  WaitForSingleObject(pid, INFINITE);
+  GetExitCodeProcess(pid, state);
+  CloseHandle(pid);
+  return pid;
+}
+#else // not windows
 #include <time.h>
 #include <stdint.h>
-#include "compat/compat.h"
+#include <unistd.h>
+#include <fcntl.h>
+int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate){
+  int flags = fcntl(fd, F_GETFL, 0);
+  if(flags < 0){
+    return -1;
+  }
+  if(oldstate){
+    *oldstate = flags & O_NONBLOCK;
+  }
+  if(state){
+    if(flags & O_NONBLOCK){
+      return 0;
+    }
+    flags |= O_NONBLOCK;
+  }else{
+    if(!(flags & O_NONBLOCK)){
+      return 0;
+    }
+    flags &= ~O_NONBLOCK;
+  }
+  if(fcntl(fd, F_SETFL, flags)){
+    return -1;
+  }
+  return 0;
+}
 #if !defined(__DragonFly_version) || __DragonFly_version < 500907
 // clock_nanosleep is unavailable on DragonFly BSD and Mac OS X
 int clock_nanosleep(clockid_t clockid, int flags, const struct timespec *request,
@@ -46,5 +87,3 @@ int clock_nanosleep(clockid_t clockid, int flags, const struct timespec *request
 }
 #endif
 #endif
-#endif
-#endif
diff --git a/src/compat/compat.h b/src/compat/compat.h
index 3d0d083710..e89f4f179d 100644
--- a/src/compat/compat.h
+++ b/src/compat/compat.h
@@ -6,6 +6,8 @@ extern "C" {
 #endif
 
 #include <time.h>
+#include <stdint.h>
+#include <sys/types.h>
 
 #define NANOSECS_IN_SEC 1000000000ul
 
@@ -14,13 +16,13 @@ extern "C" {
 #endif
 
 #ifdef  __MINGW64__
-// FIXME placeholders
+#include <Lmcons.h>
 #define tcgetattr(x, y) -1
 #define tcsetattr(x, y, z) -1
-#define sigaction(x, y, z) -1
 #define ECHO      0
 #define ICANON    0
 #define ICRNL     0
+#define INLCR     0
 #define ISIG      0
 #define TCSAFLUSH 0
 #define TCSANOW   0
@@ -33,20 +35,20 @@ extern "C" {
 typedef struct siginfo_t {
   int aieeee;
 } siginfo_t;
-typedef struct sigaction {
-  void (*sa_handler)(int);
-  void (*sa_sigaction)(int, siginfo_t *, void *);
-  sigset_t sa_mask;
-  int sa_flags;
-  void (*sa_restorer)(void);
-} sigaction;
+#define sigset_t int
 #define nl_langinfo(x) "UTF-8"
 #define ppoll(w, x, y, z) WSAPoll((w), (x), (y))
+pid_t waitpid(pid_t pid, int* ret, int flags);
+#define WNOHANG 0
 #else
+#include <sys/wait.h>
+#include <sys/time.h>
 #include <sys/mman.h>
 #include <sys/ioctl.h>
 #endif
 
+int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate);
+
 static inline uint64_t
 timespec_to_ns(const struct timespec* ts){
   return ts->tv_sec * NANOSECS_IN_SEC + ts->tv_nsec;
diff --git a/src/demo/input.c b/src/demo/input.c
index 47f2790cea..ba68cf1329 100644
--- a/src/demo/input.c
+++ b/src/demo/input.c
@@ -152,11 +152,12 @@ int demo_input_fd(void){
 // listens for events, handling mouse events directly and making other ones
 // available to demos. returns -1 if already spawned or resource failures.
 int input_dispatcher(struct notcurses* nc){
+#ifndef __MINGW64__ // FIXME
   if(input_pipefds[0] >= 0){
     return -1;
   }
   // freebsd doesn't have eventfd :/ and apple doesn't even have pipe2() =[ =[
-#ifdef __APPLE__
+#if defined(__APPLE__)
   if(pipe(input_pipefds)){
 #else
   if(pipe2(input_pipefds, O_CLOEXEC | O_NONBLOCK)){
@@ -170,6 +171,7 @@ int input_dispatcher(struct notcurses* nc){
     input_pipefds[0] = input_pipefds[1] = -1;
     return -1;
   }
+#endif
   return 0;
 }
 
diff --git a/src/fetch/main.c b/src/fetch/main.c
index 7fc2d74092..ab499af5af 100644
--- a/src/fetch/main.c
+++ b/src/fetch/main.c
@@ -1,4 +1,3 @@
-#include <pwd.h>
 #include <errno.h>
 #include <stdio.h>
 #include <fcntl.h>
@@ -10,12 +9,17 @@
 #include <sys/types.h>
 #if defined(__linux__) || defined(__gnu_hurd__)
 #include <sys/sysinfo.h>
-#else
+#include <sys/utsname.h>
+#elif !defined(__MINGW64__)
 #include <sys/sysctl.h>
-#endif
 #include <sys/utsname.h>
+#else
+#include <windows.h>
+#include <sysinfoapi.h>
+#endif
 #include <notcurses/direct.h>
 #include <notcurses/notcurses.h>
+#include "compat/compat.h"
 #include "ncart.h"
 
 typedef struct distro_info {
@@ -25,7 +29,7 @@ typedef struct distro_info {
 
 typedef struct fetched_info {
   char* username;              // we borrow a reference
-  char hostname[_POSIX_HOST_NAME_MAX];
+  char* hostname; 
   const distro_info* distro;
   char* logo;                  // strdup() from /etc/os-release
   char* distro_pretty;         // strdup() from /etc/os-release
@@ -82,7 +86,7 @@ static distro_info distros[] = {
 
 static int
 fetch_bsd_cpuinfo(fetched_info* fi){
-#if defined(__linux__) || defined(__gnu_hurd__)
+#if defined(__linux__) || defined(__gnu_hurd__) || defined(__MINGW64__)
   (void)fi;
 #else
   size_t len = sizeof(fi->core_count);
@@ -271,34 +275,6 @@ linux_ncneofetch(fetched_info* fi){
   return dinfo;
 }
 
-static int
-unix_getusername(fetched_info* fi){
-  if( (fi->username = getenv("LOGNAME")) ){
-    if( (fi->username = strdup(fi->username)) ){
-      return 0;
-    }
-  }
-  uid_t uid = getuid();
-  struct passwd* p = getpwuid(uid);
-  if(p == NULL){
-    return -1;
-  }
-  fi->username = strdup(p->pw_name);
-  return 0;
-}
-
-static int
-unix_gethostname(fetched_info* fi){
-  if(gethostname(fi->hostname, sizeof(fi->hostname)) == 0){
-    char* fqdn = strchr(fi->hostname, '.');
-    if(fqdn){
-      *fqdn = '\0';
-    }
-    return 0;
-  }
-  return -1;
-}
-
 typedef enum {
   NCNEO_LINUX,
   NCNEO_FREEBSD,
@@ -309,6 +285,7 @@ typedef enum {
 
 static ncneo_kernel_e
 get_kernel(fetched_info* fi){
+#ifndef __MINGW64__
   struct utsname uts;
   if(uname(&uts)){
     fprintf(stderr, "Failure invoking uname (%s)\n", strerror(errno));
@@ -326,6 +303,9 @@ get_kernel(fetched_info* fi){
     return NCNEO_XNU;
   }
   fprintf(stderr, "Unknown operating system via uname: %s\n", uts.sysname);
+#else
+  (void)fi;
+#endif
   return NCNEO_UNKNOWN;
 }
 
@@ -484,7 +464,11 @@ infoplane_notcurses(struct notcurses* nc, const fetched_info* fi, int planeheigh
   free(fi->term);
   ncplane_printf_aligned(infop, 4, NCALIGN_RIGHT, "Screen0: %dx%d ", fi->dimx, fi->dimy);
   ncplane_printf_aligned(infop, 5, NCALIGN_LEFT, " LANG: %s", fi->lang);
+#ifndef __MINGW64__
   ncplane_printf_aligned(infop, 5, NCALIGN_RIGHT, "UID: %ju ", (uintmax_t)getuid());
+#else
+  ncplane_printf_aligned(infop, 5, NCALIGN_RIGHT, "UID: %s ", "FIXME"); // FIXME
+#endif
   ncplane_set_styles(infop, NCSTYLE_ITALIC);
   ncplane_printf_aligned(infop, 6, NCALIGN_CENTER, "%s (%d cores)", fi->cpu_model, fi->core_count);
   nccell ul = CELL_TRIVIAL_INITIALIZER, ur = CELL_TRIVIAL_INITIALIZER;
@@ -643,8 +627,8 @@ ncneofetch(struct ncdirect* nc){
   };
   pthread_t tid;
   const bool launched = !pthread_create(&tid, NULL, display_thread, &display_marshal);
-  unix_gethostname(&fi);
-  unix_getusername(&fi);
+  fi.hostname = notcurses_hostname();
+  fi.username = notcurses_accountname();
   fetch_env_vars(nc, &fi);
   fetch_x_props(&fi);
   if(kern == NCNEO_LINUX){
diff --git a/src/info/main.c b/src/info/main.c
index 0610b70f83..c68185f2bd 100644
--- a/src/info/main.c
+++ b/src/info/main.c
@@ -378,8 +378,10 @@ tinfo_debug_bitmaps(struct ncplane* n, const tinfo* ti, const char* indent){
       }else{
         ncplane_printf(n, "%ssixel colorregs: %u", indent, ti->color_registers);
       }
+#ifdef __linux__
     }else if(ti->linux_fb_fd >= 0){
       ncplane_printf(n, "%sframebuffer graphics supported", indent);
+#endif
     }else if(ti->pixel_move == NULL){
       ncplane_printf(n, "%siTerm2 graphics support", indent);
     }else if(ti->sixel_maxy_pristine){
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 2217815c75..4a21622f2d 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -5,13 +5,13 @@
 #include <errno.h>
 #include <string.h>
 #include <unistd.h>
-#ifdef USE_READLINE
-#include <readline/readline.h>
-#endif
 #include "version.h"
 #include "visual-details.h"
 #include "notcurses/direct.h"
 #include "internal.h"
+#ifdef USE_READLINE
+#include <readline/readline.h>
+#endif
 
 // conform to the foreground and background channels of 'channels'
 static int
@@ -405,6 +405,7 @@ detect_cursor_inversion_wrapper(ncdirect* n, const char* u7, int* y, int* x){
 // the terminal, and then reading a response. many things can distupt this
 // non-atomic procedure, leading to unexpected results. a garbage function.
 int ncdirect_cursor_yx(ncdirect* n, int* y, int* x){
+#ifndef __MINGW64__ // FIXME
   struct termios termio, oldtermios;
   // this is only meaningful for real terminals
   if(n->ctermfd < 0){
@@ -455,6 +456,9 @@ int ncdirect_cursor_yx(ncdirect* n, int* y, int* x){
             n->ctermfd, strerror(errno)); // don't return error for this
   }
   return ret;
+#else
+  return -1; // FIXME
+#endif
 }
 
 int ncdirect_cursor_push(ncdirect* n){
@@ -496,13 +500,6 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
   int dimy, dimx;
   ncplane_dim_yx(np, &dimy, &dimx);
   if(np->sprite){
-    if(np->sprite->mstreamfp){
-      FILE* fp = np->sprite->mstreamfp;
-      np->sprite->mstreamfp = NULL;
-      if(fclose(fp) == EOF){
-        return -1;
-      }
-    }
     if(xoff){
       // doing an x-move without specifying the y coordinate requires asking
       // the terminal where the cursor currently is.
@@ -515,10 +512,18 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
     if(ncdirect_flush(n)){
       return -1;
     }
-    if(sprite_draw(&n->tcache, NULL, np->sprite, n->ttyfp, 0, xoff) < 0){
+    fbuf f = {};
+    if(fbuf_init(&f)){
+      return -1;
+    }
+    if(sprite_draw(&n->tcache, NULL, np->sprite, &f, 0, xoff) < 0){
+      return -1;
+    }
+    if(sprite_commit(&n->tcache, &f, np->sprite, true)){
+      fbuf_free(&f);
       return -1;
     }
-    if(sprite_commit(&n->tcache, n->ttyfp, np->sprite, true)){
+    if(fbuf_finalize(&f, n->ttyfp, true)){
       return -1;
     }
     return 0;
@@ -802,10 +807,13 @@ ncdirect_stop_minimal(void* vnc){
     rl_deprep_terminal();
 #endif
   }
-  if(nc->tcache.pixel_shutdown){
-    ret |= nc->tcache.pixel_shutdown(nc->ttyfp);
+  fbuf f = {};
+  if(fbuf_init_small(&f) == 0){
+    if(nc->tcache.pixel_shutdown){
+      ret |= nc->tcache.pixel_shutdown(&f);
+    }
+    ret |= reset_term_attributes(&nc->tcache, &f);
   }
-  ret |= reset_term_attributes(&nc->tcache, nc->ttyfp);
   if(nc->ctermfd >= 0){
     const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM);
     if(cnorm && tty_emit(cnorm, nc->ctermfd)){
@@ -857,11 +865,14 @@ ncdirect* ncdirect_core_init(const char* termtype, FILE* outfp, uint64_t flags){
   }
   // we don't need a controlling tty for everything we do; allow a failure here
   ret->ctermfd = get_tty_fd(ret->ttyfp);
+  // FIXME factor this out, and share it with rendered mode #2023
+#ifndef __MINGW64__
   int termerr;
   if(setupterm(termtype, ret->ctermfd, &termerr)){
     fprintf(stderr, "Terminfo error %d (see terminfo(3ncurses))\n", termerr);
     goto err;
   }
+#endif
   int cursor_y = -1;
   int cursor_x = -1;
   if(interrogate_terminfo(&ret->tcache, ret->ctermfd, utf8,
@@ -927,9 +938,9 @@ char* ncdirect_readline(ncdirect* n, const char* prompt){
 }
 
 static inline int
-ncdirect_style_emit(ncdirect* n, unsigned stylebits, FILE* out){
+ncdirect_style_emit(ncdirect* n, unsigned stylebits, fbuf* f){
   unsigned normalized = 0;
-  int r = coerce_styles(out, &n->tcache, &n->stylemask, stylebits, &normalized);
+  int r = coerce_styles(f, &n->tcache, &n->stylemask, stylebits, &normalized);
   // sgr0 resets colors, so set them back up if not defaults and it was used
   if(normalized){
     // emitting an sgr resets colors. if we want to be default, that's no
@@ -971,7 +982,15 @@ int ncdirect_on_styles(ncdirect* n, unsigned stylebits){
     return -1;
   }
   uint32_t stylemask = n->stylemask | stylebits;
-  if(ncdirect_style_emit(n, stylemask, n->ttyfp)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
+  if(ncdirect_style_emit(n, stylemask, &f)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fbuf_finalize(&f, n->ttyfp, false)){
     return -1;
   }
   return 0;
@@ -988,7 +1007,15 @@ unsigned ncdirect_styles(ncdirect* n){
 // turn off any specified stylebits
 int ncdirect_off_styles(ncdirect* n, unsigned stylebits){
   uint32_t stylemask = n->stylemask & ~stylebits;
-  if(ncdirect_style_emit(n, stylemask, n->ttyfp)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
+  if(ncdirect_style_emit(n, stylemask, &f)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fbuf_finalize(&f, n->ttyfp, false)){
     return -1;
   }
   return 0;
@@ -1004,7 +1031,15 @@ int ncdirect_set_styles(ncdirect* n, unsigned stylebits){
     return -1;
   }
   uint32_t stylemask = stylebits;
-  if(ncdirect_style_emit(n, stylemask, n->ttyfp)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
+  if(ncdirect_style_emit(n, stylemask, &f)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fbuf_finalize(&f, n->ttyfp, false)){
     return -1;
   }
   return 0;
@@ -1348,15 +1383,20 @@ int ncdirect_stream(ncdirect* n, const char* filename, ncstreamcb streamer,
     ncdirect_raster_frame(n, v, (vopts->flags & NCVISUAL_OPTION_HORALIGNED) ? vopts->x : 0);
     if(lastid > -1){
       if(n->tcache.pixel_remove){
-        if(n->tcache.pixel_remove(lastid, n->ttyfp)){
+        fbuf f = {};
+        fbuf_init_small(&f);
+        if(n->tcache.pixel_remove(lastid, &f) || fwrite(f.buf, f.used, 1, n->ttyfp) != 1){
+          fbuf_free(&f);
           ncvisual_destroy(ncv);
           return -1;
         }
+        fbuf_free(&f);
       }
     }
     streamer(ncv, vopts, NULL, curry);
     lastid = thisid;
   }while(ncvisual_decode(ncv) == 0);
+  ncdirect_flush(n);
   ncvisual_destroy(ncv);
   return 0;
 }
diff --git a/src/lib/fbuf.h b/src/lib/fbuf.h
index 2580dd6602..48a3dc1a5a 100644
--- a/src/lib/fbuf.h
+++ b/src/lib/fbuf.h
@@ -99,8 +99,9 @@ fbuf_initgrow(fbuf* f, unsigned small){
   // FIXME use GetLargePageMinimum() and sysconf
   size_t size = small ? (4096 > BUFSIZ ? 4096 : BUFSIZ) : 0x200000lu;
 #if defined(__linux__)
-  static bool hugepages_failed = false; // FIXME atomic
+  /*static bool hugepages_failed = false; // FIXME atomic
   if(!hugepages_failed && !small){
+    // hugepages don't seem to work with mremap() =[
     // mmap(2): hugetlb results in automatic stretch out to cover hugepage
     f->buf = (char*)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_HUGETLB |
                          MAP_PRIVATE | MAP_ANONYMOUS | MAPFLAGS , -1, 0);
@@ -109,10 +110,10 @@ fbuf_initgrow(fbuf* f, unsigned small){
       f->buf = NULL;
     }
   }
-  if(f->buf == NULL){ // try again without MAP_HUGETLB
-    f->buf = (char*)mmap(NULL, size, PROT_READ | PROT_WRITE,
-                         MAP_PRIVATE | MAP_ANONYMOUS | MAPFLAGS , -1, 0);
-  }
+  if(f->buf == NULL){ // try again without MAP_HUGETLB */
+  f->buf = (char*)mmap(NULL, size, PROT_READ | PROT_WRITE,
+                        MAP_PRIVATE | MAP_ANONYMOUS | MAPFLAGS , -1, 0);
+  //}
   if(f->buf == MAP_FAILED){
     f->buf = NULL;
     return -1;
@@ -217,7 +218,6 @@ fbuf_emit(fbuf* f, const char* esc){
     return -1;
   }
   if(fbuf_puts(f, esc) < 0){
-    //logerror("error emitting escape (%s)\n", strerror(errno));
     return -1;
   }
   return 0;
diff --git a/src/lib/fd.c b/src/lib/fd.c
index 9073e3d182..526bf4d5f7 100644
--- a/src/lib/fd.c
+++ b/src/lib/fd.c
@@ -3,12 +3,12 @@
 #include <unistd.h>
 #include <pthread.h>
 #include "internal.h"
-#ifndef __MINGW64__
-#include <sys/wait.h>
-#include <sys/time.h>
 #ifdef USING_PIDFD
 #error "USING_PIDFD was already defined; it should not be."
 #endif
+#ifdef __MINGW64__
+#include <winsock2.h>
+#endif
 #if (defined(__linux__))
 #include <linux/wait.h>
 #include <asm/unistd.h>
@@ -44,7 +44,11 @@ fdthread(ncfdplane* ncfp, int pidfd){
     pfds[1].events = NCPOLLEVENTS;
   }
   ssize_t r = 0;
+#ifndef __MINGW64__
   while(poll(pfds, fdcount, -1) >= 0 || errno == EINTR){
+#else
+  while(WSAPoll(pfds, fdcount, -1) >= 0){
+#endif
     if(pfds[0].revents){
       while((r = read(ncfp->fd, buf, BUFSIZ)) >= 0){
         if(r == 0){
@@ -80,31 +84,6 @@ ncfdplane_thread(void* vncfp){
   return NULL;
 }
 
-int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate){
-  int flags = fcntl(fd, F_GETFL, 0);
-  if(flags < 0){
-    return -1;
-  }
-  if(oldstate){
-    *oldstate = flags & O_NONBLOCK;
-  }
-  if(state){
-    if(flags & O_NONBLOCK){
-      return 0;
-    }
-    flags |= O_NONBLOCK;
-  }else{
-    if(!(flags & O_NONBLOCK)){
-      return 0;
-    }
-    flags &= ~O_NONBLOCK;
-  }
-  if(fcntl(fd, F_SETFL, flags)){
-    return -1;
-  }
-  return 0;
-}
-
 static ncfdplane*
 ncfdplane_create_internal(ncplane* n, const ncfdplane_options* opts, int fd,
                           ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn,
@@ -171,6 +150,7 @@ int ncfdplane_destroy(ncfdplane* n){
 // parent then returns.
 static pid_t
 launch_pipe_process(int* pipefd, int* pidfd){
+#ifndef __MINGW64__
   *pidfd = -1;
   int pipes[2];
 #if (defined(__linux__))
@@ -191,7 +171,6 @@ launch_pipe_process(int* pipefd, int* pidfd){
   clargs.flags = CLONE_CLEAR_SIGHAND | CLONE_FS | CLONE_PIDFD;
   clargs.exit_signal = SIGCHLD;
   p = syscall(__NR_clone3, &clargs, sizeof(clargs));
-#else
 #endif
   if(p < 0){
     p = fork();
@@ -206,12 +185,18 @@ launch_pipe_process(int* pipefd, int* pidfd){
     set_fd_nonblocking(*pipefd, 1, NULL);
   }
   return p;
+#else
+  (void)pipefd;
+  (void)pidfd;
+  return NULL;
+#endif
 }
 
 // nuke the just-spawned process, and reap it. called before the subprocess
 // reader thread is launched (which otherwise reaps the subprocess).
 static int
 kill_and_wait_subproc(pid_t pid, int pidfd, int* status){
+#ifndef __MINGW64__
   int ret = -1;
   // on linux, we try pidfd_send_signal, if the pidfd has been defined.
   // otherwise, we fall back to regular old kill();
@@ -232,6 +217,12 @@ kill_and_wait_subproc(pid_t pid, int pidfd, int* status){
     return -1;
   }
   return 0;
+#else
+  (void)pid;
+  (void)pidfd;
+  (void)status;
+  return -1;
+#endif
 }
 
 // need a poll on both main fd and pidfd
@@ -400,6 +391,9 @@ ncsubproc* ncsubproc_createvpe(ncplane* n, const ncsubproc_options* opts,
 #elif defined(__APPLE__)
     (void)env;
     execvp(bin, arg); // FIXME env?
+#elif defined(__MINGW64__)
+    (void)arg;
+    (void)env;
 #else
     exect(bin, arg, env);
 #endif
@@ -424,6 +418,7 @@ int ncsubproc_destroy(ncsubproc* n){
   if(n){
     void* vret = NULL;
 //fprintf(stderr, "pid: %u pidfd: %d waittid: %u\n", n->pid, n->pidfd, n->waittid);
+#ifndef __MINGW64__
 #ifdef USING_PIDFD
     if(n->pidfd >= 0){
       loginfo("Sending SIGKILL to pidfd %d\n", n->pidfd);
@@ -438,6 +433,7 @@ int ncsubproc_destroy(ncsubproc* n){
       kill(n->pid, SIGKILL);
     }
     pthread_mutex_unlock(&n->lock);
+#endif
 #endif
     // the thread waits on the subprocess via pidfd (iff pidfd >= 0), and
     // then exits. don't try to cancel the thread in that case; rely instead on
@@ -463,12 +459,6 @@ int ncsubproc_destroy(ncsubproc* n){
   }
   return ret;
 }
-#else
-int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate){ // FIXME
-  logerror("Not implemented for %d %u %p\n", fd, state, oldstate);
-  return -1;
-}
-#endif
 
 ncplane* ncsubproc_plane(ncsubproc* n){
   return n->nfp->ncp;
diff --git a/src/lib/input.c b/src/lib/input.c
index 69dc4cfde6..a2e0e8dc93 100644
--- a/src/lib/input.c
+++ b/src/lib/input.c
@@ -1,7 +1,9 @@
+#ifdef __MINGW64__
+#include <winsock2.h>
+#endif
 #include "input.h"
 #include "internal.h"
 #include "notcurses/direct.h"
-#include <term.h>
 #include <ctype.h>
 #include <signal.h>
 
@@ -19,6 +21,7 @@ void sigwinch_handler(int signo){
 }
 
 int cbreak_mode(int ttyfd, const struct termios* tpreserved){
+#ifndef __MINGW64__
   if(ttyfd < 0){
     return 0;
   }
@@ -37,11 +40,15 @@ int cbreak_mode(int ttyfd, const struct termios* tpreserved){
     return -1;
   }
   return 0;
+#else
+  return -1; // FIXME
+#endif
 }
 
 // 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->ttyfd < 0){
     return 0;
   }
@@ -56,11 +63,15 @@ int notcurses_linesigs_disable(notcurses* n){
     return -1;
   }
   return 0;
+#else
+  return -1; // FIXME
+#endif
 }
 
 // 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->ttyfd < 0){
     return 0;
   }
@@ -75,6 +86,9 @@ int notcurses_linesigs_enable(notcurses* n){
     return -1;
   }
   return 0;
+#else
+  return -1; // FIXME
+#endif
 }
 
 static inline int
@@ -390,18 +404,21 @@ handle_getc(ncinputlayer* nc, int kpress, ncinput* ni, int leftmargin, int topma
 
 // blocks up through ts (infinite with NULL ts), returning number of events
 // (0 on timeout) or -1 on error/interruption.
-#ifndef __MINGW64__
 static int
 block_on_input(int fd, const struct timespec* ts){
+#ifndef __MINGW64__
   struct pollfd pfd = {
     .fd = fd,
     .events = POLLIN,
     .revents = 0,
   };
-  sigset_t smask;
-  sigfillset(&smask);
-  sigdelset(&smask, SIGCONT);
-  sigdelset(&smask, SIGWINCH);
+#else
+  WSAPOLLFD pfd = {
+    .fd = fd,
+    .events = POLLIN,
+    .revents = 0,
+  };
+#endif
 #ifdef POLLRDHUP
   pfd.events |= POLLRDHUP;
 #endif
@@ -410,7 +427,16 @@ block_on_input(int fd, const struct timespec* ts){
   int timeoutms = ts ? ts->tv_sec * 1000 + ts->tv_nsec / 1000000 : -1;
   while((events = poll(&pfd, 1, timeoutms)) < 0){ // FIXME smask?
 #else
+#ifdef __MINGW64__
+  // FIXME i doubt this even works
+  while((events = WSAPoll(&pfd, 1, 0)) < 0){
+#else
+  sigset_t smask;
+  sigfillset(&smask);
+  sigdelset(&smask, SIGCONT);
+  sigdelset(&smask, SIGWINCH);
   while((events = ppoll(&pfd, 1, ts, &smask)) < 0){
+#endif
 #endif
     if(events == 0){
       return 0;
@@ -424,9 +450,6 @@ block_on_input(int fd, const struct timespec* ts){
   }
   return events;
 }
-#else
-// FIXME windows block_on_input()
-#endif
 
 static inline size_t
 input_queue_space(const ncinputlayer* nc){
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 5a929b966e..0f049dc791 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -7,10 +7,14 @@ extern "C" {
 
 #include "version.h"
 #include "builddef.h"
+#include "notcurses/notcurses.h"
+#include "compat/compat.h"
 
 #include <term.h>
 #include <ncurses.h>
 #include <time.h>
+#include <term.h>
+#include <ncurses.h> // needed for some definitions, see terminfo(3ncurses)
 #include <stdio.h>
 #include <stdint.h>
 #include <unistd.h>
@@ -26,8 +30,6 @@ extern "C" {
 #ifndef __MINGW64__
 #include <langinfo.h>
 #endif
-#include "notcurses/notcurses.h"
-#include "compat/compat.h"
 #include "termdesc.h"
 #include "egcpool.h"
 #include "sprite.h"
@@ -107,12 +109,10 @@ typedef struct ncplane {
 // current presentation state of the terminal. it is carried across render
 // instances. initialize everything to 0 on a terminal reset / startup.
 typedef struct rasterstate {
-  // we assemble the encoded (rasterized) output in a POSIX memstream, and keep
-  // it around between uses. this could be a problem if it ever tremendously
-  // spiked, but that's a highly unlikely situation.
-  char* mstream;  // buffer for rasterizing memstream, see open_memstream(3)
-  FILE* mstreamfp;// FILE* for rasterizing memstream
-  size_t mstrsize;// size of rendering memstream
+  // we assemble the encoded (rasterized) output in an fbuf (a portable POSIX
+  // memstream, basically), and keep it around between uses. this could be a
+  // problem if it ever tremendously spiked, but that seems unlikely?
+  fbuf f;         // buffer for preparing raster glyph/escape stream
 
   // the current cursor position. this is independent of whether the cursor is
   // visible. it is the cell at which the next write will take place. this is
@@ -464,7 +464,7 @@ void sigwinch_handler(int signo);
 
 void init_lang(void);
 
-int reset_term_attributes(const tinfo* ti, FILE* fp);
+int reset_term_attributes(const tinfo* ti, fbuf* f);
 
 
 // if there were missing elements we wanted from terminfo, bitch about them here
@@ -508,7 +508,7 @@ logical_to_virtual(const ncplane* n, int y){
   return (y + n->logrow) % n->leny;
 }
 
-int clear_and_home(notcurses* nc, tinfo* ti, FILE* fp, unsigned flush);
+int clear_and_home(notcurses* nc, tinfo* ti, fbuf* f);
 
 static inline int
 nfbcellidx(const ncplane* n, int row, int col){
@@ -702,9 +702,9 @@ void sprixel_hide(sprixel* s);
 sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx);
 sprixel* sprixel_recycle(ncplane* n);
 // takes ownership of s on success.
-int sprixel_load(sprixel* spx, char* s, int bytes, int pixy, int pixx, int parse_start);
+int sprixel_load(sprixel* spx, fbuf* f, int pixy, int pixx, int parse_start);
 int sprite_init(const tinfo* t, int fd);
-int sprite_clear_all(const tinfo* t, FILE* fp);
+int sprite_clear_all(const tinfo* t, fbuf* f);
 // these three all use absolute coordinates
 void sprixel_invalidate(sprixel* s, int y, int x);
 void sprixel_movefrom(sprixel* s, int y, int x);
@@ -731,17 +731,17 @@ sprite_scrub(const notcurses* n, const ncpile* p, sprixel* s){
 // precondition: s->invalidated is SPRIXEL_INVALIDATED or SPRIXEL_MOVED.
 // returns -1 on error, or the number of bytes written.
 static inline int
-sprite_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
+sprite_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
             int y, int x){
 //sprixel_debug(s, stderr);
   logdebug("sprixel %u state %d\n", s->id, s->invalidated);
-  return ti->pixel_draw(ti, p, s, out, y, x);
+  return ti->pixel_draw(ti, p, s, f, y, x);
 }
 
 // precondition: s->invalidated is SPRIXEL_MOVED or SPRIXEL_INVALIDATED
 // returns -1 on error, or the number of bytes written.
 static inline int
-sprite_redraw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
+sprite_redraw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
               int y, int x){
 //sprixel_debug(s, stderr);
   logdebug("sprixel %u state %d\n", s->id, s->invalidated);
@@ -750,21 +750,21 @@ sprite_redraw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
     // not emit it. we use sixel_maxy_pristine as a side channel to encode
     // this version information.
     bool noscroll = !ti->sixel_maxy_pristine;
-    return ti->pixel_move(s, out, noscroll);
+    return ti->pixel_move(s, f, noscroll);
   }else{
-    return ti->pixel_draw(ti, p, s, out, y, x);
+    return ti->pixel_draw(ti, p, s, f, y, x);
   }
 }
 
 // present a loaded graphic. only defined for kitty.
 static inline int
-sprite_commit(tinfo* ti, FILE* out, sprixel* s, unsigned forcescroll){
+sprite_commit(tinfo* ti, fbuf* f, sprixel* s, unsigned forcescroll){
   if(ti->pixel_commit){
     // if we are kitty prior to 0.20.0, C=1 isn't available to us, and we must
     // not emit it. we use sixel_maxy_pristine as a side channel to encode
     // this version information. direct mode, meanwhile, sets forcescroll.
     bool noscroll = !ti->sixel_maxy_pristine && !forcescroll;
-    if(ti->pixel_commit(out, s, noscroll) < 0){
+    if(ti->pixel_commit(f, s, noscroll) < 0){
       return -1;
     }
   }
@@ -1113,7 +1113,7 @@ int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate);
 // perform any work (even following a clearerr()), despite returning 0 from
 // that point on. thus, after a fflush() error, even on EAGAIN and friends,
 // you can't use the stream any further. doesn't this make fflush() pretty
-// much useless? it sure would seem to, which is why we use a memstream for
+// much useless? it sure would seem to, which is why we use an fbuf for
 // all our important I/O, which we then blit with blocking_write(). if you
 // care about your data, you'll do the same.
 static inline int
@@ -1141,19 +1141,19 @@ term_emit(const char* seq, FILE* out, bool flush){
 }
 
 static inline int
-term_bg_palindex(const notcurses* nc, FILE* out, unsigned pal){
+term_bg_palindex(const notcurses* nc, fbuf* f, unsigned pal){
   const char* setab = get_escape(&nc->tcache, ESCAPE_SETAB);
   if(setab){
-    return term_emit(tiparm(setab, pal), out, false);
+    return fbuf_emit(f, tiparm(setab, pal));
   }
   return 0;
 }
 
 static inline int
-term_fg_palindex(const notcurses* nc, FILE* out, unsigned pal){
+term_fg_palindex(const notcurses* nc, fbuf* f, unsigned pal){
   const char* setaf = get_escape(&nc->tcache, ESCAPE_SETAF);
   if(setaf){
-    return term_emit(tiparm(setaf, pal), out, false);
+    return fbuf_emit(f, tiparm(setaf, pal));
   }
   return 0;
 }
@@ -1162,7 +1162,7 @@ term_fg_palindex(const notcurses* nc, FILE* out, unsigned pal){
 // if they are different, and we have the necessary capability, write the
 // applicable terminfo entry to 'out'. returns -1 only on a true error.
 static int
-term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit,
+term_setstyle(fbuf* f, unsigned cur, unsigned targ, unsigned stylebit,
               const char* ton, const char* toff){
   int ret = 0;
   unsigned curon = cur & stylebit;
@@ -1170,11 +1170,11 @@ term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit,
   if(curon != targon){
     if(targon){
       if(ton){
-        ret = term_emit(ton, out, false);
+        ret = fbuf_emit(f, ton);
       }
     }else{
       if(toff){ // how did this happen? we can turn it on, but not off?
-        ret = term_emit(toff, out, false);
+        ret = fbuf_emit(f, toff);
       }
     }
   }
@@ -1188,26 +1188,26 @@ term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit,
 // required an sgr0 (which resets colors), normalized will be non-zero upon
 // a successful return.
 static inline int
-coerce_styles(FILE* out, const tinfo* ti, uint16_t* curstyle,
+coerce_styles(fbuf* f, const tinfo* ti, uint16_t* curstyle,
               uint16_t newstyle, unsigned* normalized){
   *normalized = 0; // we never currently use sgr0
   int ret = 0;
-  ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_BOLD,
+  ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_BOLD,
                        get_escape(ti, ESCAPE_BOLD), get_escape(ti, ESCAPE_NOBOLD));
-  ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_ITALIC,
+  ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_ITALIC,
                        get_escape(ti, ESCAPE_SITM), get_escape(ti, ESCAPE_RITM));
-  ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_STRUCK,
+  ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_STRUCK,
                        get_escape(ti, ESCAPE_SMXX), get_escape(ti, ESCAPE_RMXX));
   // underline and undercurl are exclusive. if we set one, don't go unsetting
   // the other.
   if(newstyle & NCSTYLE_UNDERLINE){ // turn on underline, or do nothing
-    ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_UNDERLINE,
+    ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERLINE,
                          get_escape(ti, ESCAPE_SMUL), get_escape(ti, ESCAPE_RMUL));
   }else if(newstyle & NCSTYLE_UNDERCURL){ // turn on undercurl, or do nothing
-    ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_UNDERCURL,
+    ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERCURL,
                          get_escape(ti, ESCAPE_SMULX), get_escape(ti, ESCAPE_SMULNOX));
   }else{ // turn off any underlining
-    ret |= term_setstyle(out, *curstyle, newstyle, NCSTYLE_UNDERCURL | NCSTYLE_UNDERLINE,
+    ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERCURL | NCSTYLE_UNDERLINE,
                          NULL, get_escape(ti, ESCAPE_RMUL));
   }
   *curstyle = newstyle;
@@ -1230,10 +1230,9 @@ mouse_enable(FILE* out){
 }
 
 static inline int
-mouse_disable(FILE* out){
-  return term_emit("\x1b[?" SET_BTN_EVENT_MOUSE ";"
-                   /*SET_FOCUS_EVENT_MOUSE ";" */SET_SGR_MODE_MOUSE "l",
-                   out, true);
+mouse_disable(fbuf* f){
+  return fbuf_emit(f, "\x1b[?" SET_BTN_EVENT_MOUSE ";"
+                   /*SET_FOCUS_EVENT_MOUSE ";" */SET_SGR_MODE_MOUSE "l");
 }
 
 // sync the drawing position to the specified location with as little overhead
@@ -1245,7 +1244,7 @@ mouse_disable(FILE* out){
 // if hardcursorpos is non-zero, we always perform a cup. this is done when we
 // don't know where the cursor currently is =].
 static inline int
-goto_location(notcurses* nc, FILE* out, int y, int x){
+goto_location(notcurses* nc, fbuf* f, int y, int x){
 //fprintf(stderr, "going to %d/%d from %d/%d hard: %u\n", y, x, nc->rstate.y, nc->rstate.x, hardcursorpos);
   int ret = 0;
   // if we don't have hpa, force a cup even if we're only 1 char away. the only
@@ -1256,13 +1255,13 @@ goto_location(notcurses* nc, FILE* out, int y, int x){
     if(nc->rstate.x == x){ // needn't move shit
       return 0;
     }
-    if(term_emit(tiparm(hpa, x), out, false)){
+    if(fbuf_emit(f, tiparm(hpa, x))){
       return -1;
     }
   }else{
     // cup is required, no need to verify existence
     const char* cup = get_escape(&nc->tcache, ESCAPE_CUP);
-    if(term_emit(tiparm(cup, y, x), out, false)){
+    if(fbuf_emit(f, tiparm(cup, y, x))){
       return -1;
     }
   }
@@ -1438,9 +1437,9 @@ egc_rtl(const char* egc, int* bytes){
 // a sprixel occupies the entirety of its associated plane, usually an entirely
 // new, purpose-specific plane. |leny| and |lenx| are output geometry in pixels.
 static inline int
-plane_blit_sixel(sprixel* spx, char* s, int bytes, int leny, int lenx,
+plane_blit_sixel(sprixel* spx, fbuf* f, int leny, int lenx,
                  int parse_start, tament* tam){
-  if(sprixel_load(spx, s, bytes, leny, lenx, parse_start)){
+  if(sprixel_load(spx, f, leny, lenx, parse_start)){
     return -1;
   }
   ncplane* n = spx->n;
@@ -1584,7 +1583,7 @@ int drop_signals(void* nc);
 int block_signals(sigset_t* old_blocked_signals);
 int unblock_signals(const sigset_t* old_blocked_signals);
 
-void ncvisual_printbanner(const notcurses* nc);
+void ncvisual_printbanner(fbuf* f);
 
 // alpha comes to us 0--255, but we have only 3 alpha values to map them to
 // (opaque, blended, and transparent). it's necessary that we display
@@ -1668,7 +1667,7 @@ ncdirect_bg_palindex_p(const ncdirect* nc){
   return ncchannels_bg_palindex_p(ncdirect_channels(nc));
 }
 
-int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b);
+int term_fg_rgb8(const tinfo* ti, fbuf* f, unsigned r, unsigned g, unsigned b);
 
 const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, bool may_degrade);
 
@@ -1746,7 +1745,7 @@ resize_bitmap(const uint32_t* bmap, int srows, int scols, size_t sstride,
 // prior to calling notcurses_core_init() (by notcurses_init()).
 typedef struct ncvisual_implementation {
   int (*visual_init)(int loglevel);
-  void (*visual_printbanner)(const struct notcurses* nc);
+  void (*visual_printbanner)(fbuf* f);
   int (*visual_blit)(struct ncvisual* ncv, int rows, int cols, ncplane* n,
                      const struct blitset* bset, const blitterargs* barg);
   struct ncvisual* (*visual_create)(void);
diff --git a/src/lib/iterm.c b/src/lib/iterm.c
index b4666a4a88..3093bc1ace 100644
--- a/src/lib/iterm.c
+++ b/src/lib/iterm.c
@@ -5,6 +5,7 @@
 #include "internal.h"
 #include "termdesc.h"
 #include "sprite.h"
+#include "fbuf.h"
 #include "png.h"
 
 // yank a cell out of the PNG by setting all of its alphas to 0. the alphas
@@ -26,47 +27,31 @@ int iterm_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 }
 
 // spit out the control sequence and data.
-int iterm_draw(const tinfo* ti, const ncpile *p, sprixel* s, FILE* out, int y, int x){
+int iterm_draw(const tinfo* ti, const ncpile *p, sprixel* s, fbuf* f, int y, int x){
   (void)ti;
   if(p){
-    if(goto_location(p->nc, out, y, x)){
+    if(goto_location(p->nc, f, y, x)){
       return -1;
     }
   }
-  if(fwrite(s->glyph, s->glyphlen, 1, out) != 1){
+  if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
     return -1;
   }
-  return s->glyphlen;
+  return s->glyph.used;
 }
 
 static int
-write_iterm_graphic(sprixel* s, const void* data, int leny, int stride, int lenx){
-  s->glyph = NULL;
-  FILE* fp = open_memstream(&s->glyph, &s->glyphlen);
-  if(fp == NULL){
+write_iterm_graphic(tament* tam, const void* data, int leny, int stride, int lenx, fbuf *f){
+  if(fbuf_puts(f, "\e]1337;File=inline=1:") < 0){
     return -1;
   }
-  if(ncfputs("\e]1337;File=inline=1:", fp) == EOF){
-    goto err;
-  }
-  // FIXME won't we need to pass TAM into write_png_b64()?
-  if(write_png_b64(data, leny, stride, lenx, fp)){
-    goto err;
-  }
-  if(ncfputs("\e\\", fp) == EOF){
-    goto err;
+  if(write_png_b64(tam, data, leny, stride, lenx, f)){
+    return -1;
   }
-  if(fclose(fp) == EOF){
-    logerror("Error flushing %zuB (%s)\n", s->glyphlen, strerror(errno));
-    free(s->glyph);
+  if(fbuf_puts(f, "\x1b\\") < 0){
     return -1;
   }
   return 0;
-
-err:
-  fclose(fp);
-  // s->glyph is freed by caller
-  return -1;
 }
 
 // create an iterm2 control sequence complete with base64-encoded PNG.
@@ -89,23 +74,28 @@ int iterm_blit(ncplane* n, int linesize, const void* data,
   if(!reuse){
     tam = malloc(sizeof(*tam) * rows * cols);
     if(tam == NULL){
-      goto error;
+      return -1;
     }
     memset(tam, 0, sizeof(*tam) * rows * cols);
   }
-  if(write_iterm_graphic(s, data, leny, linesize, lenx)){
-    goto error;
+  if(fbuf_init(&s->glyph)){
+    free(tam);
+    return -1;
+  }
+  if(write_iterm_graphic(tam, data, leny, linesize, lenx, &s->glyph)){
+    if(!reuse){
+      free(tam);
+    }
+    fbuf_free(&s->glyph);
+    return -1;
   }
   scrub_tam_boundaries(tam, leny, lenx, s->cellpxy, s->cellpxx);
-  if(plane_blit_sixel(s, s->glyph, s->glyphlen, leny, lenx, parse_start, tam) < 0){
-    goto error;
+  if(plane_blit_sixel(s, &s->glyph, leny, lenx, parse_start, tam) < 0){
+    if(!reuse){
+      free(tam);
+    }
+    fbuf_free(&s->glyph);
+    return -1;
   }
   return 1;
-
-error:
-  if(!reuse){
-    free(tam);
-  }
-  free(s->glyph);
-  return -1;
 }
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index ac8c827373..05c63d506a 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -215,16 +215,13 @@ kitty_restore(char* triplet, int skip, int max, int pleft,
 // base. we're blowing away the glyph.
 static int
 init_sprixel_animation(sprixel* s){
-  if(s->mstreamfp){
+  if(s->animating){
     return 0;
   }
-  free(s->glyph);
-  s->glyph = NULL;
-  s->glyphlen = 0;
-  if((s->mstreamfp = open_memstream(&s->glyph, &s->glyphlen)) == NULL){
-    loginfo("Opened animation buffer for sprixel %u\n", s->id);
+  if(fbuf_init(&s->glyph)){
     return -1;
   }
+  s->animating = true;
   return 0;
 }
 
@@ -243,7 +240,7 @@ int kitty_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   if((ycell + 1) * ypixels > s->pixy){
     targy = s->pixy - ycell * ypixels;
   }
-  char* c = s->glyph + s->parse_start;
+  char* c = (char*)s->glyph.buf + s->parse_start;
   int nextpixel = (s->pixx * ycell * ypixels) + (xpixels * xcell);
   int thisrow = targx;
   int chunkedhandled = 0;
@@ -355,27 +352,62 @@ int kitty_wipe_animation(sprixel* s, int ycell, int xcell){
   if(init_sprixel_animation(s)){
     return -1;
   }
-  logdebug("Wiping sprixel %u at %d/%d\n", s->id, ycell, xcell);
-  FILE* fp = s->mstreamfp;
-  fprintf(fp, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=1,q=2;",
-          xcell * s->cellpxx,
-          ycell * s->cellpxy,
-          s->cellpxx,
-          s->cellpxy,
-          s->id);
+  logdebug("wiping sprixel %u at %d/%d\n", s->id, ycell, xcell);
+  fbuf* f = &s->glyph;
+  if(fbuf_puts(f, "\x1b_Ga=f,x=") < 0){
+    return -1;
+  }
+  if(fbuf_putint(f, xcell * s->cellpxx) < 0){
+    return -1;
+  }
+  if(fbuf_puts(f, ",y=") < 0){
+    return -1;
+  }
+  if(fbuf_putint(f, ycell * s->cellpxy)){
+    return -1;
+  }
+  if(fbuf_puts(f, ",s=") < 0){
+    return -1;
+  }
+  if(fbuf_putint(f, s->cellpxx)){
+    return -1;
+  }
+  if(fbuf_puts(f, ",v=") < 0){
+    return -1;
+  }
+  if(fbuf_putint(f, s->cellpxy)){
+    return -1;
+  }
+  if(fbuf_puts(f, ",i=") < 0){
+    return -1;
+  }
+  if(fbuf_putint(f, s->id)){
+    return -1;
+  }
+  if(fbuf_puts(f, ",X=1,r=1,q=2;") < 0){
+    return -1;
+  }
   // FIXME ought be smaller around the fringes!
   int totalp = s->cellpxy * s->cellpxx;
   // FIXME preserve so long as cellpixel geom stays constant?
   for(int p = 0 ; p + 3 <= totalp ; p += 3){
-    ncfputs("AAAAAAAAAAAAAAAA", fp);
+    if(fbuf_putn(f, "AAAAAAAAAAAAAAAA", strlen("AAAAAAAAAAAAAAAA")) < 0){
+      return -1;
+    }
   }
   if(totalp % 3 == 1){
-    ncfputs("AAAAAA==", fp);
+    if(fbuf_putn(f, "AAAAAA==", strlen("AAAAAA==")) < 0){
+      return -1;
+    }
   }else if(totalp % 3 == 2){
-    ncfputs("AAAAAAAAAAA=", fp);
+    if(fbuf_putn(f, "AAAAAAAAAAA=", strlen("AAAAAAAAAAA=")) < 0){
+      return -1;
+    }
   }
   // FIXME need chunking for cells of 768+ pixels
-  ncfputs("\e\\", fp);
+  if(fbuf_putn(f, "\x1b\\", 2) < 0){
+    return -1;
+  }
   s->invalidated = SPRIXEL_INVALIDATED;
   return 1;
 }
@@ -386,26 +418,39 @@ int kitty_wipe_selfref(sprixel* s, int ycell, int xcell){
     return -1;
   }
   logdebug("Wiping sprixel %u at %d/%d\n", s->id, ycell, xcell);
-  FILE* fp = s->mstreamfp;
-  fprintf(fp, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=2,c=1,q=2;",
-          xcell * s->cellpxx,
-          ycell * s->cellpxy,
-          s->cellpxx,
-          s->cellpxy,
-          s->id);
+  fbuf* f = &s->glyph;
+  if(fbuf_printf(f, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=2,c=1,q=2;",
+                 xcell * s->cellpxx, ycell * s->cellpxy,
+                 s->cellpxx, s->cellpxy, s->id) < 0){
+    return -1;
+  }
   // FIXME ought be smaller around the fringes!
   int totalp = s->cellpxy * s->cellpxx;
   // FIXME preserve so long as cellpixel geom stays constant?
+  #define TRINULLALPHA "AAAAAAAAAAAAAAAA"
   for(int p = 0 ; p + 3 <= totalp ; p += 3){
-    ncfputs("AAAAAAAAAAAAAAAA", fp);
+    if(fbuf_putn(f, TRINULLALPHA, strlen(TRINULLALPHA)) < 0){
+      return -1;
+    }
   }
+  #undef TRINULLALPHA
   if(totalp % 3 == 1){
-    ncfputs("AAAAAA==", fp);
+  #define UNUMNULLALPHA "AAAAAA=="
+    if(fbuf_putn(f, UNUMNULLALPHA, strlen(UNUMNULLALPHA)) < 0){
+      return -1;
+    }
+  #undef UNUMNULLALPHA
   }else if(totalp % 3 == 2){
-    ncfputs("AAAAAAAAAAA=", fp);
+  #define DUONULLALPHA "AAAAAAAAAAA="
+    if(fbuf_putn(f, DUONULLALPHA, strlen(DUONULLALPHA)) < 0){
+      return -1;
+    }
+  #undef DUONULLALPHA
   }
   // FIXME need chunking for cells of 768+ pixels
-  fprintf(fp, "\e\\\e_Ga=a,i=%d,c=2,q=2;\e\\", s->id);
+  if(fbuf_printf(f, "\e\\\e_Ga=a,i=%d,c=2,q=2;\e\\", s->id) < 0){
+    return -1;
+  }
   s->invalidated = SPRIXEL_INVALIDATED;
   return 1;
 }
@@ -438,7 +483,7 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){
   if((ycell + 1) * ypixels > s->pixy){
     targy = s->pixy - ycell * ypixels;
   }
-  char* c = s->glyph + s->parse_start;
+  char* c = (char*)s->glyph.buf + s->parse_start;
 //fprintf(stderr, "TARGET AREA: %d x %d @ %dx%d of %d/%d (%d/%d) len %zu\n", targy, targx, ycell, xcell, s->dimy, s->dimx, s->pixy, s->pixx, strlen(c));
   // every pixel was 4 source bytes, 32 bits, 6.33 base64 bytes. every 3 input pixels is
   // 12 bytes (96 bits), an even 16 base64 bytes. there is chunking to worry about. there
@@ -505,9 +550,9 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){
   return -1;
 }
 
-int kitty_commit(FILE* fp, sprixel* s, unsigned noscroll){
+int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll){
   loginfo("Committing Kitty graphic id %u\n", s->id);
-  fprintf(fp, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : "");
+  fbuf_printf(f, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : "");
   s->invalidated = SPRIXEL_QUIESCENT;
   return 0;
 }
@@ -529,54 +574,75 @@ zctx_origbuf(z_stream* zctx, int pixy, int pixx){
 }
 
 static int
-encode_and_chunkify(FILE* fp, z_stream* zctx, int pixy, int pixx){
+encode_and_chunkify(fbuf* f, z_stream* zctx, int pixy, int pixx){
   unsigned long totw = zctx->total_out;
   unsigned char* buf = zctx_origbuf(zctx, pixy, pixx);
   // need to terminate the header, requiring semicolon
-  fprintf(fp, "%s;", totw > 4096 * 3 / 4 ? ",m=1" : "");
+  if(totw > 4096 * 3 / 4){
+    if(fbuf_putn(f, ",m=1", 4) < 0){
+      return -1;
+    }
+  }
+  if(fbuf_putc(f, ';') < 0){
+    return -1;
+  }
   bool first = true;
   unsigned long i = 0;
   char b64d[4];
   while(totw - i > 4096 * 3 / 4){
     if(!first){
-      ncfputs("\e_Gm=1;", fp); // successor chunk
+      if(fbuf_putn(f, "\x1b_Gm=1;", 7) < 0){
+        return -1;
+      }
     }
     unsigned long max = i + 4096 * 3 / 4;
     while(i < max){
       base64x3(buf + i, b64d);
-      fwrite(b64d, 4, 1, fp);
+      if(fbuf_putn(f, b64d, 4) < 0){
+        return -1;
+      }
       i += 3;
     }
     first = false;
-    ncfputs("\e\\", fp);
+    if(fbuf_putn(f, "\x1b\\", 2) < 0){
+      return -1;
+    }
   }
   if(!first){
-    ncfputs("\e_Gm=0;", fp);
+    if(fbuf_putn(f, "\x1b_Gm=0;", 7) < 0){
+      return -1;
+    }
   }
   while(i < totw){
     if(totw - i < 3){
       base64final(buf + i, b64d, totw - i);
-      fwrite(b64d, 4, 1, fp);
+      if(fbuf_putn(f, b64d, 4) < 0){
+        return -1;
+      }
       i += totw - i;
     }else{
       base64x3(buf + i, b64d);
-      fwrite(b64d, 4, 1, fp);
+      if(fbuf_putn(f, b64d, 4) < 0){
+        return -1;
+      }
       i += 3;
     }
   }
-  ncfputs("\e\\", fp);
+  if(fbuf_putn(f, "\x1b\\", 2) < 0){
+    return -1;
+  }
   return 0;
 }
 
 static int
-finalize_deflator(z_stream* zctx, FILE* fp, int dimy, int dimx){
+finalize_deflator(z_stream* zctx, fbuf* f, int dimy, int dimx){
   assert(0 == zctx->avail_in);
   int zret = deflate(zctx, Z_FINISH);
   if(zret != Z_STREAM_END){
     logerror("Error deflating (%d)\n", zret);
     return -1;
   }
-  if(encode_and_chunkify(fp, zctx, dimy, dimx)){
+  if(encode_and_chunkify(f, zctx, dimy, dimx)){
     return -1;
   }
   return 0;
@@ -655,7 +721,7 @@ destroy_deflator(unsigned animated, z_stream* zctx, int pixy, int pixx){
 // 16 base64-encoded bytes. 4096 / 16 == 256 3-pixel groups, or 768 pixels.
 // closes |fp| on all paths.
 static int
-write_kitty_data(FILE* fp, int linesize, int leny, int lenx, int cols,
+write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
                  const uint32_t* data, const blitterargs* bargs,
                  tament* tam, int* parse_start, kitty_graphics_e level){
 //fprintf(stderr, "drawing kitty %p\n", tam);
@@ -692,9 +758,12 @@ write_kitty_data(FILE* fp, int linesize, int leny, int lenx, int cols,
       // older versions of kitty will delete uploaded images when scrolling,
       // alas. see https://github.com/dankamongmen/notcurses/issues/1910 =[.
       // parse_start isn't used in animation mode, so no worries there.
-      *parse_start = fprintf(fp, "\e_Gf=32,s=%d,v=%d,i=%d,p=1,a=t,%s",
-                             lenx, leny, sprixelid,
-                             animated ? "o=z,q=2" : chunks ? "m=1;" : "q=2;");
+      *parse_start = fbuf_printf(f, "\e_Gf=32,s=%d,v=%d,i=%d,p=1,a=t,%s",
+                                 lenx, leny, sprixelid,
+                                 animated ? "o=z,q=2" : chunks ? "m=1;" : "q=2;");
+      if(*parse_start < 0){
+        goto err;
+      }
       // so if we're animated, we've printed q=2, but no semicolon to close
       // the control block, since we're not yet sure what m= to write. we've
       // otherwise written q=2; if we're the only chunk, and m=1; otherwise.
@@ -702,7 +771,9 @@ write_kitty_data(FILE* fp, int linesize, int leny, int lenx, int cols,
       // handled following deflate.
     }else{
       if(!animated){
-        fprintf(fp, "\e_G%sm=%d;", chunks ? "" : "q=2,", chunks ? 1 : 0);
+        if(fbuf_printf(f, "\e_G%sm=%d;", chunks ? "" : "q=2,", chunks ? 1 : 0) < 0){
+          goto err;
+        }
       }
     }
     if((targetout += RGBA_MAXLEN) > total){
@@ -798,15 +869,19 @@ write_kitty_data(FILE* fp, int linesize, int leny, int lenx, int cols,
         // we already took transcolor to alpha 0; there's no need to
         // check it again, so pass 0.
         base64_rgba3(source, encodeable, out, wipe, 0);
-        ncfputs(out, fp);
+        if(fbuf_puts(f, out) < 0){
+          goto err;
+        }
       }
     }
     if(!animated){
-      ncfputs("\e\\", fp);
+      if(fbuf_putn(f, "\x1b\\", 2) < 0){
+        goto err;
+      }
     }
   }
   if(animated){
-    if(finalize_deflator(&zctx, fp, leny, lenx)){
+    if(finalize_deflator(&zctx, f, leny, lenx)){
       goto err;
     }
   }
@@ -826,16 +901,16 @@ int kitty_rebuild_selfref(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   if(init_sprixel_animation(s)){
     return -1;
   }
-  FILE* fp = s->mstreamfp;
+  fbuf* f = &s->glyph;
   const int ystart = ycell * s->cellpxy;
   const int xstart = xcell * s->cellpxx;
   const int xlen = xstart + s->cellpxx > s->pixx ? s->pixx - xstart : s->cellpxx;
   const int ylen = ystart + s->cellpxy > s->pixy ? s->pixy - ystart : s->cellpxy;
   logdebug("rematerializing %u at %d/%d (%dx%d)\n", s->id, ycell, xcell, ylen, xlen);
-  fprintf(fp, "\e_Ga=c,x=%d,y=%d,X=%d,Y=%d,w=%d,h=%d,i=%d,r=1,c=2,q=2;\x1b\\",
-          xcell * s->cellpxx, ycell * s->cellpxy,
-          xcell * s->cellpxx, ycell * s->cellpxy,
-          xlen, ylen, s->id);
+  fbuf_printf(f, "\e_Ga=c,x=%d,y=%d,X=%d,Y=%d,w=%d,h=%d,i=%d,r=1,c=2,q=2;\x1b\\",
+              xcell * s->cellpxx, ycell * s->cellpxy,
+              xcell * s->cellpxx, ycell * s->cellpxy,
+              xlen, ylen, s->id);
   const int tyx = xcell + ycell * s->dimx;
   memcpy(&s->n->tam[tyx].state, auxvec, sizeof(s->n->tam[tyx].state));
   s->invalidated = SPRIXEL_INVALIDATED;
@@ -846,8 +921,8 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   if(init_sprixel_animation(s)){
     return -1;
   }
-  FILE* fp = s->mstreamfp;
-  logdebug("rebuilding %u at %d/%d\n", s->id, ycell, xcell);
+  fbuf* f = &s->glyph;
+  logdebug("rebuilding sprixel %u %d at %d/%d\n", s->id, s->invalidated, ycell, xcell);
   const int ystart = ycell * s->cellpxy;
   const int xstart = xcell * s->cellpxx;
   const int xlen = xstart + s->cellpxx > s->pixx ? s->pixx - xstart : s->cellpxx;
@@ -862,14 +937,32 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   int targetout = 0; // number of pixels expected out after this chunk
 //fprintf(stderr, "total: %d chunks = %d, s=%d,v=%d\n", total, chunks, lenx, leny);
   // FIXME this ought be factored out and shared with write_kitty_data()
-  logdebug("Placing %d/%d at %d/%d\n", ylen, xlen, ycell * s->cellpxy, xcell * s->cellpxx);
+  logdebug("placing %d/%d at %d/%d\n", ylen, xlen, ycell * s->cellpxy, xcell * s->cellpxx);
   while(chunks--){
     if(totalout == 0){
-      fprintf(fp, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=1,%s;",
-              xcell * s->cellpxx, ycell * s->cellpxy, xlen, ylen,
-              s->id, chunks ? "m=1" : "q=2");
+      if(fbuf_printf(f, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=1,%s;",
+                     xcell * s->cellpxx, ycell * s->cellpxy, xlen, ylen,
+                     s->id, chunks ? "m=1" : "q=2") < 0){
+        return -1;
+      }
     }else{
-      fprintf(fp, "\e_G%sm=%d;", chunks ? "" : "q=2,", chunks ? 1 : 0);
+      if(fbuf_putn(f, "\x1b_G", 3) < 0){
+        return -1;
+      }
+      if(!chunks){
+        if(fbuf_putn(f, "q=2,", 4) < 0){
+          return -1;
+        }
+      }
+      if(fbuf_putn(f, "m=", 2) < 0){
+        return -1;
+      }
+      if(fbuf_putint(f, chunks ? 1 : 0) < 0){
+        return -1;
+      }
+      if(fbuf_putc(f, ';') != 1){
+        return -1;
+      }
     }
     if((targetout += RGBA_MAXLEN) > total){
       targetout = total;
@@ -909,9 +1002,13 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
       totalout += encodeable;
       char out[17];
       base64_rgba3(source, encodeable, out, wipe, 0);
-      ncfputs(out, fp);
+      if(fbuf_puts(f, out) < 0){
+        return -1;
+      }
+    }
+    if(fbuf_putn(f, "\x1b\\", 2) < 0){
+      return -1;
     }
-    ncfputs("\e\\", fp);
   }
 //fprintf(stderr, "EMERGED WITH TAM STATE %d\n", s->n->tam[tyx].state);
   s->invalidated = SPRIXEL_INVALIDATED;
@@ -923,7 +1020,7 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 // deflate-compressed) 24bit RGB. Returns -1 on error, 1 on success.
 static inline int
 kitty_blit_core(ncplane* n, int linesize, const void* data, int leny, int lenx,
-                    const blitterargs* bargs, kitty_graphics_e level){
+                const blitterargs* bargs, kitty_graphics_e level){
 //fprintf(stderr, "IMAGE: start %p end %p\n", data, (const char*)data + leny * linesize);
   int cols = bargs->u.pixel.spx->dimx;
   int rows = bargs->u.pixel.spx->dimy;
@@ -949,31 +1046,25 @@ kitty_blit_core(ncplane* n, int linesize, const void* data, int leny, int lenx,
     }
     memset(tam, 0, sizeof(*tam) * rows * cols);
   }
-  if(write_kitty_data(s->mstreamfp, linesize, leny, lenx, cols, data,
+  fbuf* f = &s->glyph;
+  if(write_kitty_data(f, linesize, leny, lenx, cols, data,
                       bargs, tam, &parse_start, level)){
     goto error;
   }
   if(level == KITTY_ALWAYS_SCROLLS){
-    if(fclose(s->mstreamfp)){
-      goto error;
-    }
-    s->mstreamfp = NULL;
+    s->animating = false;
   }
   // take ownership of |buf| and |tam| on success.
-  if(plane_blit_sixel(s, s->glyph, s->glyphlen, leny, lenx, parse_start, tam) < 0){
+  if(plane_blit_sixel(s, &s->glyph, leny, lenx, parse_start, tam) < 0){
     goto error;
   }
   return 1;
 
 error:
-  if(s->mstreamfp){
-    fclose(s->mstreamfp);
-  }
   if(!reuse){
     free(tam);
   }
-  free(s->glyph);
-  s->glyph = NULL;
+  fbuf_free(&s->glyph);
   return -1;
 }
 
@@ -995,9 +1086,9 @@ int kitty_blit_selfref(ncplane* n, int linesize, const void* data,
                          KITTY_SELFREF);
 }
 
-int kitty_remove(int id, FILE* out){
+int kitty_remove(int id, fbuf* f){
   loginfo("Removing graphic %u\n", id);
-  if(fprintf(out, "\e_Ga=d,d=i,i=%d\e\\", id) < 0){
+  if(fbuf_printf(f, "\e_Ga=d,d=i,i=%d\e\\", id) < 0){
     return -1;
   }
   return 0;
@@ -1034,32 +1125,26 @@ int kitty_scrub(const ncpile* p, sprixel* s){
 }
 
 // returns the number of bytes written
-int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
+int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
                int y, int x){
   (void)ti;
   (void)p;
   (void)y;
   (void)x;
   bool animated = false;
-  if(s->mstreamfp){ // active animation
-    int fret = fclose(s->mstreamfp);
-    s->mstreamfp = NULL;
-    if(fret == EOF){
-      return -1;
-    }
+  if(s->animating){ // active animation
+    s->animating = false;
     animated = true;
   }
-  int ret = s->glyphlen;
-  logdebug("Writing out %zub for %u\n", s->glyphlen, s->id);
+  int ret = s->glyph.used;
+  logdebug("Writing out %zub for %u\n", s->glyph.used, s->id);
   if(ret){
-    if(fwrite(s->glyph, s->glyphlen, 1, out) != 1){
+    if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
       ret = -1;
     }
   }
   if(animated){
-    free(s->glyph);
-    s->glyph = NULL;
-    s->glyphlen = 0;
+    fbuf_free(&s->glyph);
     s->invalidated = SPRIXEL_LOADED;
   }else{
     s->invalidated = SPRIXEL_LOADED;
@@ -1068,10 +1153,10 @@ int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
 }
 
 // returns -1 on failure, 0 on success (move bytes do not count for sprixel stats)
-int kitty_move(sprixel* s, FILE* out, unsigned noscroll){
+int kitty_move(sprixel* s, fbuf* f, unsigned noscroll){
   int ret = 0;
-  if(fprintf(out, "\e_Ga=p,i=%d,p=1,q=2%s\e\\", s->id,
-             noscroll ? ",C=1" : "") < 0){
+  if(fbuf_printf(f, "\e_Ga=p,i=%d,p=1,q=2%s\e\\", s->id,
+                 noscroll ? ",C=1" : "") < 0){
     ret = -1;
   }
   s->invalidated = SPRIXEL_QUIESCENT;
@@ -1079,14 +1164,17 @@ int kitty_move(sprixel* s, FILE* out, unsigned noscroll){
 }
 
 // clears all kitty bitmaps
-int kitty_clear_all(FILE* fp){
+int kitty_clear_all(fbuf* f){
 //fprintf(stderr, "KITTY UNIVERSAL ERASE\n");
-  return term_emit("\e_Ga=d,q=2\e\\", fp, false);
+  if(fbuf_putn(f, "\x1b_Ga=d,q=2\x1b\\", 12) < 0){
+    return -1;
+  }
+  return 0;
 }
 
-int kitty_shutdown(FILE* fp){
+int kitty_shutdown(fbuf* f){
   // FIXME need to close off any open kitty bitmap emission, or we will
   // lock up the terminal
-  (void)fp;
+  (void)f;
   return 0;
 }
diff --git a/src/lib/linux.c b/src/lib/linux.c
index 79d2a453a0..edefa59cbd 100644
--- a/src/lib/linux.c
+++ b/src/lib/linux.c
@@ -8,7 +8,7 @@ int fbcon_wipe(sprixel* s, int ycell, int xcell){
   if(auxvec == NULL){
     return -1;
   }
-  char* glyph = s->glyph;
+  char* glyph = s->glyph.buf;
   for(int y = 0 ; y < s->cellpxy ; ++y){
     if(ycell * s->cellpxy + y >= s->pixy){
       break;
@@ -29,44 +29,6 @@ int fbcon_wipe(sprixel* s, int ycell, int xcell){
   return 0;
 }
 
-int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
-  if(auxvec == NULL){
-    return -1;
-  }
-  sprixcell_e state = SPRIXCELL_TRANSPARENT;
-  for(int y = 0 ; y < s->cellpxy ; ++y){
-    if(ycell * s->cellpxy + y >= s->pixy){
-      break;
-    }
-    const size_t yoff = s->pixx * (ycell * s->cellpxy + y);
-    for(int x = 0 ; x < s->cellpxx ; ++x){
-      if(xcell * s->cellpxx + x >= s->pixx){
-        break;
-      }
-      size_t offset = (yoff + xcell * s->cellpxx + x) * 4;
-      const int vyx = (y % s->cellpxy) * s->cellpxx + x;
-      if(x == 0 && y == 0){
-        if(auxvec[vyx] == 0){
-          state = SPRIXCELL_TRANSPARENT;
-        }else{
-          state = SPRIXCELL_OPAQUE_SIXEL;
-        }
-      }else{
-        if(auxvec[vyx] == 0 && state == SPRIXCELL_OPAQUE_SIXEL){
-          state = SPRIXCELL_MIXED_SIXEL;
-        }else if(auxvec[vyx] && state == SPRIXCELL_TRANSPARENT){
-          state = SPRIXCELL_MIXED_SIXEL;
-        }
-      }
-      s->glyph[offset + 3] = auxvec[vyx];
-      offset += 4;
-    }
-  }
-  s->n->tam[s->dimx * ycell + xcell].state = state;
-  s->invalidated = SPRIXEL_INVALIDATED;
-  return 1;
-}
-
 int fbcon_blit(struct ncplane* n, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs){
   uint32_t transcolor = bargs->transcolor;
@@ -75,9 +37,10 @@ int fbcon_blit(struct ncplane* n, int linesize, const void* data,
   sprixel* s = bargs->u.pixel.spx;
   int cdimx = s->cellpxx;
   int cdimy = s->cellpxy;
-  s->glyphlen = leny * lenx * 4;
-  s->glyph = malloc(s->glyphlen);
-  if(s->glyph == NULL){
+  // FIXME this will need be a copy of the tinfo's fbuf map
+  size_t flen = leny * lenx * 4;
+  s->glyph.buf = malloc(flen);
+  if(s->glyph.buf == NULL){
     return -1;
   }
   tament* tam = NULL;
@@ -100,7 +63,7 @@ int fbcon_blit(struct ncplane* n, int linesize, const void* data,
     size_t soffset = l * linesize;
     const uint8_t* src = (const unsigned char*)data + soffset;
     size_t toffset = l * lenx * 4;
-    char* dst = s->glyph + toffset;
+    char* dst = (char *)s->glyph.buf + toffset;
     for(int c = 0 ; c < lenx ; ++c){
       int xcell = c / cdimx;
       int tyx = xcell + ycell * bargs->u.pixel.spx->dimx;
@@ -142,7 +105,7 @@ int fbcon_blit(struct ncplane* n, int linesize, const void* data,
     }
   }
   scrub_tam_boundaries(tam, leny, lenx, cdimy, cdimx);
-  if(plane_blit_sixel(s, s->glyph, s->glyphlen, leny, lenx, 0, tam) < 0){
+  if(plane_blit_sixel(s, &s->glyph, leny, lenx, 0, tam) < 0){
     goto error;
   }
   return 1;
@@ -151,7 +114,8 @@ int fbcon_blit(struct ncplane* n, int linesize, const void* data,
   if(!reuse){
     free(tam);
   }
-  free(s->glyph);
+  free(s->glyph.buf);
+  s->glyph.size = 0;
   return -1;
 }
 
@@ -159,15 +123,61 @@ int fbcon_scrub(const struct ncpile* p, sprixel* s){
   return sixel_scrub(p, s);
 }
 
-int fbcon_draw(const tinfo* ti, const struct ncpile *p, sprixel* s, FILE* out, int y, int x){
+#ifdef __linux__
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <linux/fb.h>
+#include <linux/kd.h>
+#include <sys/ioctl.h>
+
+int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
+  if(auxvec == NULL){
+    return -1;
+  }
+  sprixcell_e state = SPRIXCELL_TRANSPARENT;
+  for(int y = 0 ; y < s->cellpxy ; ++y){
+    if(ycell * s->cellpxy + y >= s->pixy){
+      break;
+    }
+    const size_t yoff = s->pixx * (ycell * s->cellpxy + y);
+    for(int x = 0 ; x < s->cellpxx ; ++x){
+      if(xcell * s->cellpxx + x >= s->pixx){
+        break;
+      }
+      size_t offset = (yoff + xcell * s->cellpxx + x) * 4;
+      const int vyx = (y % s->cellpxy) * s->cellpxx + x;
+      if(x == 0 && y == 0){
+        if(auxvec[vyx] == 0){
+          state = SPRIXCELL_TRANSPARENT;
+        }else{
+          state = SPRIXCELL_OPAQUE_SIXEL;
+        }
+      }else{
+        if(auxvec[vyx] == 0 && state == SPRIXCELL_OPAQUE_SIXEL){
+          state = SPRIXCELL_MIXED_SIXEL;
+        }else if(auxvec[vyx] && state == SPRIXCELL_TRANSPARENT){
+          state = SPRIXCELL_MIXED_SIXEL;
+        }
+      }
+      s->glyph.buf[offset + 3] = auxvec[vyx];
+      offset += 4;
+    }
+  }
+  s->n->tam[s->dimx * ycell + xcell].state = state;
+  s->invalidated = SPRIXEL_INVALIDATED;
+  return 1;
+}
+
+int fbcon_draw(const tinfo* ti, const struct ncpile *p, sprixel* s, fbuf* f, int y, int x){
   (void)p;
-  (void)out; // we don't write to the stream
+  (void)f; // we don't write to the stream
   int wrote = 0;
   for(unsigned l = 0 ; l < (unsigned)s->pixy && l < ti->pixy ; ++l){
     // FIXME pixel size isn't necessarily 4B, line isn't necessarily psize*pixx
     size_t offset = ((l + y * ti->cellpixy) * ti->pixx + x * ti->cellpixx) * 4;
     uint8_t* tl = ti->linux_fbuffer + offset;
-    const char* src = s->glyph + (l * s->pixx * 4);
+    const char* src = (char*)s->glyph.buf + (l * s->pixx * 4);
     for(unsigned c = 0 ; c < (unsigned)s->pixx && c < ti->pixx ; ++c){
       uint32_t pixel;
       memcpy(&pixel, src, 4);
@@ -182,14 +192,6 @@ int fbcon_draw(const tinfo* ti, const struct ncpile *p, sprixel* s, FILE* out, i
   return wrote;
 }
 
-#ifdef __linux__
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <linux/fb.h>
-#include <linux/kd.h>
-#include <sys/ioctl.h>
-
 // each row is a contiguous set of bits, starting at the msb
 static inline size_t
 row_bytes(const struct console_font_op* cfo){
@@ -659,16 +661,22 @@ bool is_linux_framebuffer(tinfo* ti){
   return true;
 }
 #else
-bool is_linux_console(int fd, unsigned no_font_changes, bool* quadrants){
-  (void)fd;
-  (void)no_font_changes;
-  (void)quadrants;
-  return false;
+int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
+  (void)s;
+  (void)ycell;
+  (void)xcell;
+  (void)auxvec;
+  return 0;
 }
 
-bool is_linux_framebuffer(tinfo* ti){
+int fbcon_draw(const tinfo* ti, const struct ncpile *p, sprixel* s, fbuf* f, int y, int x){
   (void)ti;
-  return false;
+  (void)p;
+  (void)s;
+  (void)f;
+  (void)y;
+  (void)x;
+  return 0;
 }
 
 int get_linux_fb_pixelgeom(tinfo* ti, unsigned* ypix, unsigned *xpix){
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index f101abda14..04b801cafb 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -5,7 +5,6 @@
 #include "internal.h"
 #include <zlib.h>
 #include <time.h>
-#include <term.h>
 #include <fcntl.h>
 #include <errno.h>
 #include <stdio.h>
@@ -31,19 +30,19 @@ void notcurses_version_components(int* major, int* minor, int* patch, int* tweak
 
 // reset the current colors, styles, and palette. called on startup (to purge
 // any preexisting styling) and shutdown (to not affect further programs).
-int reset_term_attributes(const tinfo* ti, FILE* fp){
+int reset_term_attributes(const tinfo* ti, fbuf* f){
   int ret = 0;
   const char* esc;
-  if((esc = get_escape(ti, ESCAPE_RESTORECOLORS)) && term_emit(esc, fp, false)){
+  if((esc = get_escape(ti, ESCAPE_RESTORECOLORS)) && fbuf_emit(f, esc)){
     ret = -1;
   }
-  if((esc = get_escape(ti, ESCAPE_OP)) && term_emit(esc, fp, false)){
+  if((esc = get_escape(ti, ESCAPE_OP)) && fbuf_emit(f, esc)){
     ret = -1;
   }
-  if((esc = get_escape(ti, ESCAPE_SGR0)) && term_emit(esc, fp, false)){
+  if((esc = get_escape(ti, ESCAPE_SGR0)) && fbuf_emit(f, esc)){
     ret = -1;
   }
-  if((esc = get_escape(ti, ESCAPE_OC)) && term_emit(esc, fp, false)){
+  if((esc = get_escape(ti, ESCAPE_OC)) && fbuf_emit(f, esc)){
     ret = -1;
   }
   return ret;
@@ -58,8 +57,8 @@ notcurses_stop_minimal(void* vnc){
   notcurses* nc = vnc;
   // collect output into the memstream buffer, and then dump it directly using
   // blocking_write(), to avoid problems with unreliable fflush().
-  FILE* out = nc->rstate.mstreamfp;
-  fseeko(out, 0, SEEK_SET);
+  fbuf* f = &nc->rstate.f;
+  fbuf_reset(f);
   int ret = 0;
   ret |= drop_signals(nc);
   // be sure to write the restoration sequences *prior* to running rmcup, as
@@ -69,32 +68,29 @@ notcurses_stop_minimal(void* vnc){
   // byte. if we leave an active escape open, it can lock up the terminal.
   // we only want to do it when in the middle of a rasterization, though. FIXME
   if(nc->tcache.pixel_shutdown){
-    ret |= nc->tcache.pixel_shutdown(out);
+    ret |= nc->tcache.pixel_shutdown(f);
   }
-  ret |= mouse_disable(out);
-  ret |= reset_term_attributes(&nc->tcache, out);
+  ret |= mouse_disable(f);
+  ret |= reset_term_attributes(&nc->tcache, f);
   if(nc->ttyfd >= 0){
     if((esc = get_escape(&nc->tcache, ESCAPE_RMCUP))){
-      if(sprite_clear_all(&nc->tcache, out)){
+      if(sprite_clear_all(&nc->tcache, f)){
         ret = -1;
       }
-      if(term_emit(esc, out, false)){
+      if(fbuf_emit(f, esc)){
         ret = -1;
       }
     }
     ret |= tcsetattr(nc->ttyfd, TCSANOW, &nc->tcache.tpreserved);
   }
-  if((esc = get_escape(&nc->tcache, ESCAPE_RMKX)) && term_emit(esc, out, false)){
+  if((esc = get_escape(&nc->tcache, ESCAPE_RMKX)) && fbuf_emit(f, esc)){
     ret = -1;
   }
   const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM);
-  if(cnorm && term_emit(cnorm, out, false)){
+  if(cnorm && fbuf_emit(f, cnorm)){
     ret = -1;
   }
-  if(fflush(out)){
-    return -1;
-  }
-  return blocking_write(fileno(nc->ttyfp), nc->rstate.mstream, nc->rstate.mstrsize);
+  return blocking_write(fileno(nc->ttyfp), f->buf, f->used);
 }
 
 // make a heap-allocated wchar_t expansion of the multibyte string at s
@@ -262,9 +258,12 @@ int update_term_dimensions(int fd, int* rows, int* cols, tinfo* tcache,
     *cols = ws.ws_col;
   }
   if(tcache){
+#ifdef __linux__
     if(tcache->linux_fb_fd >= 0){
       get_linux_fb_pixelgeom(tcache, &tcache->pixy, &tcache->pixx);
-    }else{
+    }else
+#endif
+    {
       // we might have the pixel geometry from CSI14t, so don't override a
       // valid earlier response with 0s from the ioctl. we do want to fire
       // off a fresh CSI14t in this case, though FIXME.
@@ -881,13 +880,13 @@ int ncplane_destroy_family(ncplane *ncp){
 // only invoked without suppress banners flag. prints various warnings based on
 // the environment / terminal definition. returns the number of lines printed.
 static int
-init_banner_warnings(const notcurses* nc, FILE* out){
-  term_fg_palindex(nc, out, nc->tcache.caps.colors <= 88 ? 1 : 0xcb);
+init_banner_warnings(const notcurses* nc, fbuf* f){
+  term_fg_palindex(nc, f, nc->tcache.caps.colors <= 88 ? 1 : 0xcb);
   if(!notcurses_canutf8(nc)){
-    fprintf(out, " Warning! Encoding is not UTF-8; output may be degraded.\n");
+    fbuf_puts(f, " Warning! Encoding is not UTF-8; output may be degraded.\n");
   }
   if(!get_escape(&nc->tcache, ESCAPE_HPA)){
-    fprintf(out, " Warning! No absolute horizontal placement.\n");
+    fbuf_puts(f, " Warning! No absolute horizontal placement.\n");
   }
   return 0;
 }
@@ -895,41 +894,43 @@ init_banner_warnings(const notcurses* nc, FILE* out){
 // unless the suppress_banner flag was set, print some version information and
 // (if applicable) warnings to ttyfp. we are not yet on the alternate screen.
 static int
-init_banner(const notcurses* nc){
+init_banner(const notcurses* nc, fbuf* f){
   if(!nc->suppress_banner){
     char prefixbuf[BPREFIXSTRLEN + 1];
-    term_fg_palindex(nc, stdout, 50 % nc->tcache.caps.colors);
-    fprintf(nc->ttyfp, "notcurses %s on %s %s\n", notcurses_version(),
-            nc->tcache.termname ? nc->tcache.termname : "?",
-            nc->tcache.termversion ? nc->tcache.termversion : "");
-    term_fg_palindex(nc, stdout, nc->tcache.caps.colors <= 256 ?
+    term_fg_palindex(nc, f, 50 % nc->tcache.caps.colors);
+    fbuf_printf(f, "notcurses %s on %s %s\n", notcurses_version(),
+                nc->tcache.termname ? nc->tcache.termname : "?",
+                nc->tcache.termversion ? nc->tcache.termversion : "");
+    term_fg_palindex(nc, f, nc->tcache.caps.colors <= 256 ?
                      14 % nc->tcache.caps.colors : 0x2080e0);
     if(nc->tcache.cellpixy && nc->tcache.cellpixx){
-      fprintf(nc->ttyfp, "%d rows (%dpx) %d cols (%dpx) %dx%d %zuB crend %u colors",
-              nc->stdplane->leny, nc->tcache.cellpixy,
-              nc->stdplane->lenx, nc->tcache.cellpixx,
-              nc->stdplane->leny * nc->tcache.cellpixy,
-              nc->stdplane->lenx * nc->tcache.cellpixx,
-              sizeof(struct crender), nc->tcache.caps.colors);
+      fbuf_printf(f, "%d rows (%dpx) %d cols (%dpx) %dx%d %luB crend %u colors",
+                  nc->stdplane->leny, nc->tcache.cellpixy,
+                  nc->stdplane->lenx, nc->tcache.cellpixx,
+                  nc->stdplane->leny * nc->tcache.cellpixy,
+                  nc->stdplane->lenx * nc->tcache.cellpixx,
+                  (unsigned long)sizeof(struct crender),
+                  nc->tcache.caps.colors);
     }else{
-      fprintf(nc->ttyfp, "%d rows %d cols (%sB) %zuB crend %d colors",
-              nc->stdplane->leny, nc->stdplane->lenx,
-              bprefix(nc->stats.s.fbbytes, 1, prefixbuf, 0),
-              sizeof(struct crender), nc->tcache.caps.colors);
+      fbuf_printf(f, "%d rows %d cols (%sB) %luB crend %u colors",
+                  nc->stdplane->leny, nc->stdplane->lenx,
+                  bprefix(nc->stats.s.fbbytes, 1, prefixbuf, 0),
+                  (unsigned long)sizeof(struct crender),
+                  nc->tcache.caps.colors);
     }
     const char* setaf;
     if(nc->tcache.caps.rgb && (setaf = get_escape(&nc->tcache, ESCAPE_SETAF))){
-      ncfputc('+', nc->ttyfp);
-      term_fg_rgb8(&nc->tcache, stdout, 0xe0, 0x60, 0x60);
-      ncfputc('R', nc->ttyfp);
-      term_fg_rgb8(&nc->tcache, stdout, 0x60, 0xe0, 0x60);
-      ncfputc('G', nc->ttyfp);
-      term_fg_rgb8(&nc->tcache, stdout, 0x20, 0x80, 0xff);
-      ncfputc('B', nc->ttyfp);
-      term_fg_palindex(nc, nc->ttyfp, nc->tcache.caps.colors <= 256 ?
+      fbuf_putc(f, '+');
+      term_fg_rgb8(&nc->tcache, f, 0xe0, 0x60, 0x60);
+      fbuf_putc(f, 'r');
+      term_fg_rgb8(&nc->tcache, f, 0x60, 0xe0, 0x60);
+      fbuf_putc(f, 'g');
+      term_fg_rgb8(&nc->tcache, f, 0x20, 0x80, 0xff);
+      fbuf_putc(f, 'b');
+      term_fg_palindex(nc, f, nc->tcache.caps.colors <= 256 ?
                        14 % nc->tcache.caps.colors : 0x2080e0);
     }
-    fprintf(nc->ttyfp, "\n%s%s, %zuB %s cells\nterminfo from %s zlib %s\n",
+    fbuf_printf(f, "\n%s%s, %zuB %s cells\nterminfo from %s zlib %s\n",
 #ifdef __clang__
             "", // name is contained in __VERSION__
 #else
@@ -951,12 +952,12 @@ init_banner(const notcurses* nc){
 #error "No __BYTE_ORDER__ definition"
 #endif
             curses_version(), zlibVersion());
-    ncvisual_printbanner(nc);
-    init_banner_warnings(nc, nc->ttyfp);
+    ncvisual_printbanner(f);
+    init_banner_warnings(nc, f);
     const char* esc;
     if( (esc = get_escape(&nc->tcache, ESCAPE_SGR0)) ||
         (esc = get_escape(&nc->tcache, ESCAPE_OP))){
-      term_emit(esc, nc->ttyfp, false);
+      fbuf_emit(f, esc);
     }
   }
   return 0;
@@ -1050,8 +1051,9 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     return ret;
   }
   ret->last_pile = NULL;
-  ret->rstate.mstream = NULL;
-  ret->rstate.mstreamfp = NULL;
+  ret->rstate.f.buf = NULL;
+  ret->rstate.f.used = 0;
+  ret->rstate.f.size = 0;
   ret->loglevel = opts->loglevel;
   if(!(opts->flags & NCOPTION_INHIBIT_SETLOCALE)){
     init_lang();
@@ -1102,9 +1104,9 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     free(ret);
     return NULL;
   }
-  // mstreamfp is needed by notcurses_stop_minimal, so this must be done
+  // the fbuf is needed by notcurses_stop_minimal, so this must be done
   // before registering fatal signal handlers.
-  if((ret->rstate.mstreamfp = open_memstream(&ret->rstate.mstream, &ret->rstate.mstrsize)) == NULL){
+  if(fbuf_init(&ret->rstate.f)){
     pthread_mutex_destroy(&ret->pilelock);
     pthread_mutex_destroy(&ret->stats.lock);
     free(ret);
@@ -1113,7 +1115,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
   if(setup_signals(ret, (opts->flags & NCOPTION_NO_QUIT_SIGHANDLERS),
                    (opts->flags & NCOPTION_NO_WINCH_SIGHANDLER),
                    notcurses_stop_minimal)){
-    fclose(ret->rstate.mstreamfp);
+    fbuf_free(&ret->rstate.f);
     pthread_mutex_destroy(&ret->pilelock);
     pthread_mutex_destroy(&ret->stats.lock);
     free(ret);
@@ -1122,16 +1124,20 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
   // don't set loglevel until we've acquired the signal handler, lest we
   // change the loglevel out from under a running instance
   loglevel = opts->loglevel;
+#ifndef __MINGW64__
+// windows doesn't really have a concept of terminfo. you might ssh into other
+// machines, but they'll use the terminfo installed thereon (putty, etc.).
   int termerr;
   if(setupterm(opts->termtype, ret->ttyfd, &termerr) != OK){
     logpanic("Terminfo error %d (see terminfo(3ncurses))\n", termerr);
     drop_signals(ret);
-    fclose(ret->rstate.mstreamfp);
+    fbuf_free(&ret->rstate.f);
     pthread_mutex_destroy(&ret->stats.lock);
     pthread_mutex_destroy(&ret->pilelock);
     free(ret);
     return NULL;
   }
+#endif
   ret->rstate.logendy = -1;
   ret->rstate.logendx = -1;
   ret->rstate.x = ret->rstate.y = -1;
@@ -1151,7 +1157,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     // the u7 led the queries so that we would get a cursor position
     // unaffected by any query spill (unconsumed control sequences). move
     // us back to that location, in case there was any such spillage.
-    if(goto_location(ret, ret->ttyfp, *cursory, *cursorx)){
+    if(goto_location(ret, &ret->rstate.f, *cursory, *cursorx)){
       goto err;
     }
   }
@@ -1168,7 +1174,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     logerror("Couldn't create the initial plane (bad margins?)\n");
     goto err;
   }
-  reset_term_attributes(&ret->tcache, ret->ttyfp);
+  reset_term_attributes(&ret->tcache, &ret->rstate.f);
   const char* smkx = get_escape(&ret->tcache, ESCAPE_SMKX);
   if(smkx && term_emit(smkx, ret->ttyfp, false)){
     free_plane(ret->stdplane);
@@ -1184,7 +1190,10 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     free_plane(ret->stdplane);
     goto err;
   }
-  init_banner(ret);
+  ret->rstate.x = ret->rstate.y = -1;
+  init_banner(ret, &ret->rstate.f);
+  fwrite(ret->rstate.f.buf, ret->rstate.f.used, 1, ret->ttyfp);
+  fbuf_reset(&ret->rstate.f);
   if(ncflush(ret->ttyfp)){
     free_plane(ret->stdplane);
     goto err;
@@ -1217,7 +1226,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
       // perform an explicit clear since the alternate screen was requested
       // (smcup *might* clear, but who knows? and it might not have been
       // available in any case).
-      if(clear_and_home(ret, &ret->tcache, ret->ttyfp, true)){
+      if(clear_and_home(ret, &ret->tcache, &ret->rstate.f)){
         goto err;
       }
       // no need to reestablish a preserved cursor -- that only affects the
@@ -1227,8 +1236,14 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
   // the sprite clear ought take place within the alternate screen, if it's
   // being used.
   if(!(opts->flags & NCOPTION_NO_CLEAR_BITMAPS)){
-    if(sprite_clear_all(&ret->tcache, ret->ttyfp) || fflush(ret->ttyfp)){
-      free_plane(ret->stdplane);
+    if(sprite_clear_all(&ret->tcache, &ret->rstate.f)){
+      goto err;
+    }
+  }
+  if(ret->rstate.f.used){
+    fwrite(ret->rstate.f.buf, ret->rstate.f.used, 1, ret->ttyfp);
+    fbuf_reset(&ret->rstate.f);
+    if(ncflush(ret->ttyfp)){
       goto err;
     }
   }
@@ -1237,10 +1252,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
 err:
   logpanic("Alas, you will not be going to space today.\n");
   // FIXME looks like we have some memory leaks on this error path?
-  if(ret->rstate.mstreamfp){
-    fclose(ret->rstate.mstreamfp);
-  }
-  free(ret->rstate.mstream);
+  fbuf_free(&ret->rstate.f);
   tcsetattr(ret->ttyfd, TCSANOW, &ret->tcache.tpreserved);
   drop_signals(ret);
   pthread_mutex_destroy(&ret->stats.lock);
@@ -1294,10 +1306,12 @@ int notcurses_stop(notcurses* nc){
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       if(++targy >= nc->lfdimy){
-        printf("\v");
+        printf("\n");
         --targy;
       }
-      goto_location(nc, stdout, targy, 0);
+      fbuf_reset(&nc->rstate.f);
+      goto_location(nc, &nc->rstate.f, targy, 0);
+      fwrite(nc->rstate.f.buf, nc->rstate.f.used, 1, stdout);
       fflush(stdout);
     }
     ret |= set_fd_nonblocking(nc->tcache.input.infd, nc->stdio_blocking_save, NULL);
@@ -1305,21 +1319,20 @@ int notcurses_stop(notcurses* nc){
       notcurses_drop_planes(nc);
       free_plane(nc->stdplane);
     }
-    if(nc->rstate.mstreamfp){
-      fclose(nc->rstate.mstreamfp);
-    }
     if(nc->ttyfd >= 0){
       ret |= close(nc->ttyfd);
     }
     egcpool_dump(&nc->pool);
     free(nc->lastframe);
-    free(nc->rstate.mstream);
+    fbuf_free(&nc->rstate.f);
     // get any current stats loaded into stash_stats
     notcurses_stats_reset(nc, NULL);
     if(!nc->suppress_banner){
       summarize_stats(nc);
     }
+#ifndef __MINGW64__
     del_curterm(cur_term);
+#endif
     ret |= pthread_mutex_destroy(&nc->stats.lock);
     ret |= pthread_mutex_destroy(&nc->pilelock);
     free_terminfo_cache(&nc->tcache);
@@ -2226,9 +2239,19 @@ int notcurses_mouse_enable(notcurses* n){
 // this seems to work (note difference in suffix, 'l' vs 'h'), but what about
 // the sequences 1000 etc?
 int notcurses_mouse_disable(notcurses* n){
-  if(mouse_disable(n->ttyfp)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
+  if(mouse_disable(&f)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fwrite(f.buf, f.used, 1, n->ttyfp) != 1 || fflush(n->ttyfp) == EOF){
+    fbuf_free(&f);
     return -1;
   }
+  fbuf_free(&f);
   return 0;
 }
 
diff --git a/src/lib/png.c b/src/lib/png.c
index 9cd5c29205..c2b4a9fbce 100644
--- a/src/lib/png.c
+++ b/src/lib/png.c
@@ -4,6 +4,7 @@
 #include "visual-details.h"
 #include "internal.h"
 #include "base64.h"
+#include "fbuf.h"
 #include "png.h"
 
 // http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
@@ -191,7 +192,7 @@ struct b64ctx {
 };
 
 static int
-fwrite64(const void* src, size_t osize, FILE* fp, struct b64ctx* bctx){
+fbuf_putn64(fbuf* f, const void* src, size_t osize, struct b64ctx* bctx){
   size_t w = 0;
   char b64[4];
   if(bctx->srcidx){
@@ -205,7 +206,7 @@ fwrite64(const void* src, size_t osize, FILE* fp, struct b64ctx* bctx){
     memcpy(bctx->src + bctx->srcidx, src, copy);
     base64x3(bctx->src, b64);
     bctx->srcidx = 0;
-    if(fwrite(b64, 4, 1, fp) != 1){
+    if(fbuf_putn(f, b64, 4) != 4){
       return -1;
     }
     w = copy;
@@ -213,7 +214,7 @@ fwrite64(const void* src, size_t osize, FILE* fp, struct b64ctx* bctx){
   // the bctx is now guaranteed to be empty
   while(w + 3 <= osize){
     base64x3((const unsigned char*)src + w, b64);
-    if(fwrite(b64, 4, 1, fp) != 1){
+    if(fbuf_putn(f, b64, 4) != 4){
       return -1;
     }
     w += 3;
@@ -227,7 +228,7 @@ fwrite64(const void* src, size_t osize, FILE* fp, struct b64ctx* bctx){
 }
 
 static size_t
-fwrite_idats(FILE* fp, const unsigned char* data, size_t dlen,
+fwrite_idats(tament* tam, fbuf* f, const unsigned char* data, size_t dlen,
              struct b64ctx* bctx){
   static const char ctype[] = "IDAT";
   uint32_t written = 0;
@@ -238,11 +239,13 @@ fwrite_idats(FILE* fp, const unsigned char* data, size_t dlen,
       thischunk = CHUNK_MAX_DATA;
     }
     uint32_t nclen = htonl(thischunk);
-    if(fwrite64(&nclen, 4, fp, bctx) != 1 ||
-       fwrite64(ctype, 4, fp, bctx) != 1 ||
-       fwrite64(data + dwritten, thischunk, fp, bctx) != 1){
+    if(fbuf_putn64(f, &nclen, 4, bctx) != 1 ||
+       fbuf_putn64(f, ctype, 4, bctx) != 1 ||
+       fbuf_putn64(f, data + dwritten, thischunk, bctx) != 1){
       return 0;
     }
+    // FIXME use the TAM!
+    (void)tam;
 // FIXME horrible; PoC; do not retain!
 unsigned char* crcbuf = malloc(thischunk + 8);
 memcpy(crcbuf, &nclen, 4);
@@ -251,7 +254,7 @@ memcpy(crcbuf + 8, data + dwritten, thischunk);
 // END horribleness
     uint32_t crc = chunk_crc(crcbuf);
 free(crcbuf); // FIXME well a bit more
-    if(fwrite64(&crc, 4, fp, bctx) != 1){
+    if(fbuf_putn64(f, &crc, 4, bctx) != 1){
       return 0;
     }
     dlen -= thischunk;
@@ -261,7 +264,8 @@ free(crcbuf); // FIXME well a bit more
   return written;
 }
 
-int write_png_b64(const void* data, int rows, int rowstride, int cols, FILE* fp){
+int write_png_b64(tament* tam, const void* data, int rows, int rowstride,
+                  int cols, fbuf* f){
   void* deflated;
   size_t dlen;
   compute_png_size(data, rows, rowstride, cols, &deflated, &dlen);
@@ -269,28 +273,28 @@ int write_png_b64(const void* data, int rows, int rowstride, int cols, FILE* fp)
     return -1;
   }
   struct b64ctx bctx = { };
-  if(fwrite64(PNGHEADER, sizeof(PNGHEADER) - 1, fp, &bctx) != 1){
+  if(fbuf_putn64(f, PNGHEADER, sizeof(PNGHEADER) - 1, &bctx) < 0){
     free(deflated);
     return -1;
   }
   unsigned char ihdr[25];
   write_ihdr(rows, cols, ihdr);
-  if(fwrite64(ihdr, sizeof(ihdr), fp, &bctx) != 1){
+  if(fbuf_putn64(f, ihdr, sizeof(ihdr), &bctx) != 1){
     free(deflated);
     return -1;
   }
-  if(fwrite_idats(fp, deflated, dlen, &bctx) == 0){
+  if(fwrite_idats(tam, f, deflated, dlen, &bctx) == 0){
     free(deflated);
     return -1;
   }
   free(deflated);
-  if(fwrite64(IEND, sizeof(IEND) - 1, fp, &bctx) != 1){
+  if(fbuf_putn64(f, IEND, sizeof(IEND) - 1, &bctx) != 1){
     return -1;
   }
   if(bctx.srcidx){
     char b64[4];
     base64final(bctx.src, b64, bctx.srcidx);
-    if(fwrite(b64, 4, 1, fp) != 1){
+    if(fbuf_putn(f, b64, 4) < 0){
       return -1;
     }
   }
diff --git a/src/lib/png.h b/src/lib/png.h
index 4a0f853001..85c7968242 100644
--- a/src/lib/png.h
+++ b/src/lib/png.h
@@ -7,11 +7,12 @@ extern "C" {
 
 #include <stdio.h>
 
-struct ncvisual;
+struct fbuf;
+struct tament;
 
 // create the PNG, encode it using base64, and write it to |fp|
-int write_png_b64(const void* data, int rows, int rowstride, int cols,
-                  FILE* fp);
+int write_png_b64(struct tament* tam, const void* data, int rows, int rowstride,
+                  int cols, struct fbuf* f);
 
 #ifdef __cplusplus
 }
diff --git a/src/lib/render.c b/src/lib/render.c
index e090643bea..6821a01331 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -542,19 +542,19 @@ int ncplane_mergedown_simple(ncplane* restrict src, ncplane* restrict dst){
 
 // write the nccell's UTF-8 extended grapheme cluster to the provided FILE*.
 static int
-term_putc(FILE* out, const egcpool* e, const nccell* c){
+term_putc(fbuf* f, const egcpool* e, const nccell* c){
   if(cell_simple_p(c)){
 //fprintf(stderr, "[%.4s] %08x\n", (const char*)&c->gcluster, c->gcluster); }
     // we must not have any 'cntrl' characters at this point
     if(c->gcluster == 0){
-      if(ncfputc(' ', out) == EOF){
+      if(fbuf_putc(f, ' ') < 0){
         return -1;
       }
-    }else if(ncfputs((const char*)&c->gcluster, out) == EOF){
+    }else if(fbuf_puts(f, (const char*)&c->gcluster) == EOF){
       return -1;
     }
   }else{
-    if(ncfputs(egcpool_extended_gcluster(e, c), out) == EOF){
+    if(fbuf_puts(f, egcpool_extended_gcluster(e, c)) == EOF){
       return -1;
     }
   }
@@ -563,9 +563,9 @@ term_putc(FILE* out, const egcpool* e, const nccell* c){
 
 // write any escape sequences necessary to set the desired style
 static inline int
-term_setstyles(FILE* out, notcurses* nc, const nccell* c){
+term_setstyles(fbuf* f, notcurses* nc, const nccell* c){
   unsigned normalized = false;
-  int ret = coerce_styles(out, &nc->tcache, &nc->rstate.curattr,
+  int ret = coerce_styles(f, &nc->tcache, &nc->rstate.curattr,
                           nccell_styles(c), &normalized);
   if(normalized){
     nc->rstate.fgdefelidable = true;
@@ -598,15 +598,15 @@ static const char* const NUMBERS[] = {
 "241;", "242;", "243;", "244;", "245;", "246;", "247;", "248;", "249;", "250;", "251;", "252;", "253;", "254;", "255;", };
 
 static inline int
-term_esc_rgb(FILE* out, bool foreground, unsigned r, unsigned g, unsigned b){
+term_esc_rgb(fbuf* f, bool foreground, unsigned r, unsigned g, unsigned b){
   // The correct way to do this is using tiparm+tputs, but doing so (at least
   // as of terminfo 6.1.20191019) both emits ~3% more bytes for a run of 'rgb'
   // and gives rise to some inaccurate colors (possibly due to special handling
   // of values < 256; I'm not at this time sure). So we just cons up our own.
   /*if(esc == 4){
-    return term_emit("setab", tiparm(nc->setab, (int)((r << 16u) | (g << 8u) | b)), out, false);
+    return fbuf_emit(f, "setab", tiparm(nc->setab, (int)((r << 16u) | (g << 8u) | b)));
   }else if(esc == 3){
-    return term_emit("setaf", tiparm(nc->setaf, (int)((r << 16u) | (g << 8u) | b)), out, false);
+    return fbuf_emit(f, "setaf", tiparm(nc->setaf, (int)((r << 16u) | (g << 8u) | b)));
   }else{
     return -1;
   }*/
@@ -615,37 +615,28 @@ term_esc_rgb(FILE* out, bool foreground, unsigned r, unsigned g, unsigned b){
   // not supported by several terminal emulators :/.
   #define RGBESC2 "8;2;"
   // fprintf() was sitting atop our profiles, so we put the effort into a fast solution
-  // here. assemble a buffer using constants and a lookup table. we can use 20
-  // bytes in the worst case.
-  char rgbbuf[20] = RGBESC1 " " RGBESC2;
-  if(foreground){
-    rgbbuf[2] = '3';
-  }else{
-    rgbbuf[2] = '4';
-  }
-  size_t offset = 7;
+  // here. assemble a buffer using constants and a lookup table.
+  fbuf_putn(f, RGBESC1, strlen(RGBESC1));
+  fbuf_putc(f, foreground ? '3' : '4');
+  fbuf_putn(f, RGBESC2, strlen(RGBESC2));
   const char* s = NUMBERS[r];
   while(*s){
-    rgbbuf[offset++] = *s++;
+    fbuf_putc(f, *s++);
   }
   s = NUMBERS[g];
   while(*s){
-    rgbbuf[offset++] = *s++;
+    fbuf_putc(f, *s++);
   }
   s = NUMBERS[b];
   while(*s != ';'){
-    rgbbuf[offset++] = *s++;
-  }
-  rgbbuf[offset++] = 'm';
-  rgbbuf[offset] = '\0';
-  if(fwrite(rgbbuf, offset, 1, out) != 1){
-    return -1;
+    fbuf_putc(f, *s++);
   }
+  fbuf_putc(f, 'm');
   return 0;
 }
 
 static inline int
-term_bg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
+term_bg_rgb8(const tinfo* ti, fbuf* f, unsigned r, unsigned g, unsigned b){
   // We typically want to use tputs() and tiperm() to acquire and write the
   // escapes, as these take into account terminal-specific delays, padding,
   // etc. For the case of DirectColor, there is no suitable terminfo entry, but
@@ -663,7 +654,7 @@ term_bg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
         }
       }
     }
-    return term_esc_rgb(out, false, r, g, b);
+    return term_esc_rgb(f, false, r, g, b);
   }else{
     const char* setab = get_escape(ti, ESCAPE_SETAB);
     if(setab){
@@ -672,23 +663,23 @@ term_bg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
       // a single screen, start... combining close ones? For 8-color mode, simple
       // interpolation. I have no idea what to do for 88 colors. FIXME
       if(ti->caps.colors >= 256){
-        return term_emit(tiparm(setab, rgb_quantize_256(r, g, b)), out, false);
+        return fbuf_emit(f, tiparm(setab, rgb_quantize_256(r, g, b)));
       }else if(ti->caps.colors >= 8){
-        return term_emit(tiparm(setab, rgb_quantize_8(r, g, b)), out, false);
+        return fbuf_emit(f, tiparm(setab, rgb_quantize_8(r, g, b)));
       }
     }
   }
   return 0;
 }
 
-int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
+int term_fg_rgb8(const tinfo* ti, fbuf* f, unsigned r, unsigned g, unsigned b){
   // We typically want to use tputs() and tiperm() to acquire and write the
   // escapes, as these take into account terminal-specific delays, padding,
   // etc. For the case of DirectColor, there is no suitable terminfo entry, but
   // we're also in that case working with hopefully more robust terminals.
   // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself.
   if(ti->caps.rgb){
-    return term_esc_rgb(out, true, r, g, b);
+    return term_esc_rgb(f, true, r, g, b);
   }else{
     const char* setaf = get_escape(ti, ESCAPE_SETAF);
     if(setaf){
@@ -697,9 +688,9 @@ int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b)
       // a single screen, start... combining close ones? For 8-color mode, simple
       // interpolation. I have no idea what to do for 88 colors. FIXME
       if(ti->caps.colors >= 256){
-        return term_emit(tiparm(setaf, rgb_quantize_256(r, g, b)), out, false);
+        return fbuf_emit(f, tiparm(setaf, rgb_quantize_256(r, g, b)));
       }else if(ti->caps.colors >= 8){
-        return term_emit(tiparm(setaf, rgb_quantize_8(r, g, b)), out, false);
+        return fbuf_emit(f, tiparm(setaf, rgb_quantize_8(r, g, b)));
       }
     }
   }
@@ -707,7 +698,7 @@ int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b)
 }
 
 static inline int
-update_palette(notcurses* nc, FILE* out){
+update_palette(notcurses* nc, fbuf* f){
   if(nc->tcache.caps.can_change_colors){
     const char* initc = get_escape(&nc->tcache, ESCAPE_INITC);
     for(size_t damageidx = 0 ; damageidx < sizeof(nc->palette.chans) / sizeof(*nc->palette.chans) ; ++damageidx){
@@ -719,7 +710,9 @@ update_palette(notcurses* nc, FILE* out){
         r = r * 1000 / 255;
         g = g * 1000 / 255;
         b = b * 1000 / 255;
-        term_emit(tiparm(initc, damageidx, r, g, b), out, false);
+        if(fbuf_emit(f, tiparm(initc, damageidx, r, g, b)) < 0){
+          return -1;
+        }
         nc->palette_damage[damageidx] = false;
       }
     }
@@ -730,7 +723,7 @@ update_palette(notcurses* nc, FILE* out){
 // at least one of the foreground and background are the default. emit the
 // necessary return to default (if one is necessary), and update rstate.
 static inline int
-raster_defaults(notcurses* nc, bool fgdef, bool bgdef, FILE* out){
+raster_defaults(notcurses* nc, bool fgdef, bool bgdef, fbuf* f){
   const char* op = get_escape(&nc->tcache, ESCAPE_OP);
   if(op == NULL){ // if we don't have op, we don't have fgop/bgop
     return 0;
@@ -743,7 +736,7 @@ raster_defaults(notcurses* nc, bool fgdef, bool bgdef, FILE* out){
     ++nc->stats.s.defaultelisions;
     return 0;
   }else if((mustsetfg && mustsetbg) || !fgop || !bgop){
-    if(term_emit(op, out, false)){
+    if(fbuf_emit(f, op)){
       return -1;
     }
     nc->rstate.fgdefelidable = true;
@@ -753,14 +746,14 @@ raster_defaults(notcurses* nc, bool fgdef, bool bgdef, FILE* out){
     nc->rstate.fgpalelidable = false;
     nc->rstate.bgpalelidable = false;
   }else if(mustsetfg){ // if we reach here, we must have fgop
-    if(term_emit(fgop, out, false)){
+    if(fbuf_emit(f, fgop)){
       return -1;
     }
     nc->rstate.fgdefelidable = true;
     nc->rstate.fgelidable = false;
     nc->rstate.fgpalelidable = false;
   }else{ // mustsetbg and !mustsetfg and bgop != NULL
-    if(term_emit(bgop, out, false)){
+    if(fbuf_emit(f, bgop)){
       return -1;
     }
     nc->rstate.bgdefelidable = true;
@@ -773,13 +766,13 @@ raster_defaults(notcurses* nc, bool fgdef, bool bgdef, FILE* out){
 
 // these are unlikely, so we leave it uninlined
 static int
-emit_fg_palindex(notcurses* nc, FILE* out, const nccell* srccell){
+emit_fg_palindex(notcurses* nc, fbuf* f, const nccell* srccell){
   unsigned palfg = nccell_fg_palindex(srccell);
   // we overload lastr for the palette index; both are 8 bits
   if(nc->rstate.fgpalelidable && nc->rstate.lastr == palfg){
     ++nc->stats.s.fgelisions;
   }else{
-    if(term_fg_palindex(nc, out, palfg)){
+    if(term_fg_palindex(nc, f, palfg)){
       return -1;
     }
     ++nc->stats.s.fgemissions;
@@ -792,12 +785,12 @@ emit_fg_palindex(notcurses* nc, FILE* out, const nccell* srccell){
 }
 
 static int
-emit_bg_palindex(notcurses* nc, FILE* out, const nccell* srccell){
+emit_bg_palindex(notcurses* nc, fbuf* f, const nccell* srccell){
   unsigned palbg = nccell_bg_palindex(srccell);
   if(nc->rstate.bgpalelidable && nc->rstate.lastbr == palbg){
     ++nc->stats.s.bgelisions;
   }else{
-    if(term_bg_palindex(nc, out, palbg)){
+    if(term_bg_palindex(nc, f, palbg)){
       return -1;
     }
     ++nc->stats.s.bgemissions;
@@ -822,7 +815,7 @@ emit_bg_palindex(notcurses* nc, FILE* out, const nccell* srccell){
 // are loaded, but old kitty graphics remain visible, and new/updated kitty
 // graphics are not yet visible, and they have not moved.
 static int64_t
-clean_sprixels(notcurses* nc, ncpile* p, FILE* out){
+clean_sprixels(notcurses* nc, ncpile* p, fbuf* f){
   sprixel* s;
   sprixel** parent = &p->sprixelcache;
   int64_t bytesemitted = 0;
@@ -859,10 +852,10 @@ clean_sprixels(notcurses* nc, ncpile* p, FILE* out){
       }
       // FIXME kitty graphics don't need the goto_location before a load, but only
       // before a presentation; we ought be able to eliminate this
-      if(goto_location(nc, out, y + nc->margin_t, x + nc->margin_l)){
+      if(goto_location(nc, f, y + nc->margin_t, x + nc->margin_l)){
         return -1;
       }
-      int r = sprite_redraw(&nc->tcache, p, s, out, y + nc->margin_t, x + nc->margin_l);
+      int r = sprite_redraw(&nc->tcache, p, s, f, y + nc->margin_t, x + nc->margin_l);
       if(r < 0){
         return -1;
       }
@@ -882,11 +875,11 @@ clean_sprixels(notcurses* nc, ncpile* p, FILE* out){
 
 // "%d tardies to work off, by far the most in the class!\n", p->scrolls
 static int
-rasterize_scrolls(ncpile* p, FILE* out){
+rasterize_scrolls(ncpile* p, fbuf* f){
   if(p->scrolls == 0){
     return 0;
   }
-  logdebug("Order-%d scroll\n", p->scrolls);
+  logdebug("order-%d scroll\n", p->scrolls);
   if(p->nc->rstate.logendy >= 0){
     p->nc->rstate.logendy -= p->scrolls;
     if(p->nc->rstate.logendy < 0){
@@ -894,11 +887,11 @@ rasterize_scrolls(ncpile* p, FILE* out){
       p->nc->rstate.logendx = 0;
     }
   }
-  if(goto_location(p->nc, out, p->dimy, 0)){
+  if(goto_location(p->nc, f, p->dimy, 0)){
     return -1;
   }
   while(p->scrolls){
-    if(ncfputc('\n', out) < 0){
+    if(fbuf_putc(f, '\n') < 0){
       return -1;
     }
     --p->scrolls;
@@ -916,7 +909,7 @@ rasterize_scrolls(ncpile* p, FILE* out){
 //
 // don't account for sprixelemissions here, as they were already counted.
 static int64_t
-rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
+rasterize_sprixels(notcurses* nc, ncpile* p, fbuf* f){
   int64_t bytesemitted = 0;
   sprixel* s;
   sprixel** parent = &p->sprixelcache;
@@ -926,7 +919,7 @@ rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
 //fprintf(stderr, "3 DRAWING BITMAP %d STATE %d AT %d/%d for %p\n", s->id, s->invalidated, y + nc->margin_t, x + nc->margin_l, s->n);
       int y,x;
       ncplane_yx(s->n, &y, &x);
-      int r = sprite_draw(&nc->tcache, p, s, out, y + nc->margin_t, x + nc->margin_l);
+      int r = sprite_draw(&nc->tcache, p, s, f, y + nc->margin_t, x + nc->margin_l);
       if(r < 0){
         return -1;
       }
@@ -936,17 +929,17 @@ rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
       if(nc->tcache.pixel_commit){
         int y,x;
         ncplane_yx(s->n, &y, &x);
-        if(goto_location(nc, out, y + nc->margin_t, x + nc->margin_l)){
+        if(goto_location(nc, f, y + nc->margin_t, x + nc->margin_l)){
           return -1;
         }
-        if(sprite_commit(&nc->tcache, out, s, false)){
+        if(sprite_commit(&nc->tcache, f, s, false)){
           return -1;
         }
         nc->rstate.hardcursorpos = true;
       }
     }else if(s->invalidated == SPRIXEL_HIDE){
       if(nc->tcache.pixel_remove){
-        if(nc->tcache.pixel_remove(s->id, out) < 0){
+        if(nc->tcache.pixel_remove(s->id, f) < 0){
           return -1;
         }
         if( (*parent = s->next) ){
@@ -973,7 +966,7 @@ rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
 // lastframe has *not yet been written to the screen*, i.e. it's only about to
 // *become* the last frame rasterized.
 static int
-rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
+rasterize_core(notcurses* nc, const ncpile* p, fbuf* f, unsigned phase){
   struct crender* rvec = p->crender;
   // we only need to emit a coordinate if it was damaged. the damagemap is a
   // bit per coordinate, one per struct crender.
@@ -997,12 +990,12 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
         // was not above a sprixel (and the cell is damaged). in the second
         // phase, we draw everything that remains damaged.
         ++nc->stats.s.cellemissions;
-        if(goto_location(nc, out, y, x)){
+        if(goto_location(nc, f, y, x)){
           return -1;
         }
         // set the style. this can change the color back to the default; if it
         // does, we need update our elision possibilities.
-        if(term_setstyles(out, nc, srccell)){
+        if(term_setstyles(f, nc, srccell)){
           return -1;
         }
         // if our cell has a default foreground *or* background, we can elide
@@ -1013,7 +1006,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
         bool nobackground = cell_nobackground_p(srccell);
         if((nccell_fg_default_p(srccell)) || (!nobackground && nccell_bg_default_p(srccell))){
           if(raster_defaults(nc, nccell_fg_default_p(srccell),
-                            !nobackground && nccell_bg_default_p(srccell), out)){
+                             !nobackground && nccell_bg_default_p(srccell), f)){
             return -1;
           }
         }
@@ -1022,7 +1015,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
         //  * the previous was non-default, and matches what we have now, or
         //  * we are a no-foreground glyph (iswspace() is true)
         if(nccell_fg_palindex_p(srccell)){ // palette-indexed foreground
-          if(emit_fg_palindex(nc, out, srccell)){
+          if(emit_fg_palindex(nc, f, srccell)){
             return -1;
           }
         }else if(!nccell_fg_default_p(srccell)){ // rgb foreground
@@ -1030,7 +1023,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
           if(nc->rstate.fgelidable && nc->rstate.lastr == r && nc->rstate.lastg == g && nc->rstate.lastb == b){
             ++nc->stats.s.fgelisions;
           }else{
-            if(term_fg_rgb8(&nc->tcache, out, r, g, b)){
+            if(term_fg_rgb8(&nc->tcache, f, r, g, b)){
               return -1;
             }
             ++nc->stats.s.fgemissions;
@@ -1047,7 +1040,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
         if(nobackground){
           ++nc->stats.s.bgelisions;
         }else if(nccell_bg_palindex_p(srccell)){ // palette-indexed background
-          if(emit_bg_palindex(nc, out, srccell)){
+          if(emit_bg_palindex(nc, f, srccell)){
             return -1;
           }
         }else if(!nccell_bg_default_p(srccell)){ // rgb background
@@ -1055,7 +1048,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
           if(nc->rstate.bgelidable && nc->rstate.lastbr == br && nc->rstate.lastbg == bg && nc->rstate.lastbb == bb){
             ++nc->stats.s.bgelisions;
           }else{
-            if(term_bg_rgb8(&nc->tcache, out, br, bg, bb)){
+            if(term_bg_rgb8(&nc->tcache, f, br, bg, bb)){
               return -1;
             }
             ++nc->stats.s.bgemissions;
@@ -1076,7 +1069,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
             sprixel_invalidate(rvec[damageidx].sprixel, y, x);
           }
         }
-        if(term_putc(out, &nc->pool, srccell)){
+        if(term_putc(f, &nc->pool, srccell)){
           return -1;
         }
         rvec[damageidx].s.damaged = 0;
@@ -1098,26 +1091,26 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
 // assuming success, it is non-0 if application-synchronized updates are
 // desired; in this case, a SUM footer is present at the end of the buffer.
 static int
-notcurses_rasterize_inner(notcurses* nc, ncpile* p, FILE* out, unsigned* asu){
+notcurses_rasterize_inner(notcurses* nc, ncpile* p, fbuf* f, unsigned* asu){
   logdebug("pile %p ymax: %d xmax: %d\n", p, p->dimy + nc->margin_t, p->dimx + nc->margin_l);
   // don't write a clearscreen. we only update things that have been changed.
   // we explicitly move the cursor at the beginning of each output line, so no
   // need to home it expliticly.
-  update_palette(nc, out);
-  if(rasterize_scrolls(p, out)){
+  update_palette(nc, f);
+  if(rasterize_scrolls(p, f)){
     return -1;
   }
   logdebug("Sprixel phase 1\n");
-  int64_t sprixelbytes = clean_sprixels(nc, p, out);
+  int64_t sprixelbytes = clean_sprixels(nc, p, f);
   if(sprixelbytes < 0){
     return -1;
   }
   logdebug("Glyph phase 1\n");
-  if(rasterize_core(nc, p, out, 0)){
+  if(rasterize_core(nc, p, f, 0)){
     return -1;
   }
   logdebug("Sprixel phase 2\n");
-  int64_t rasprixelbytes = rasterize_sprixels(nc, p, out);
+  int64_t rasprixelbytes = rasterize_sprixels(nc, p, f);
   if(rasprixelbytes < 0){
     return -1;
   }
@@ -1126,19 +1119,15 @@ notcurses_rasterize_inner(notcurses* nc, ncpile* p, FILE* out, unsigned* asu){
     nc->stats.s.sprixelbytes += sprixelbytes;
   pthread_mutex_unlock(&nc->stats.lock);
   logdebug("Glyph phase 2\n");
-  if(rasterize_core(nc, p, out, 1)){
-    return -1;
-  }
-  // need to flush before doing SUMode size check, to update mstrsize
-  if(ncflush(out)){
+  if(rasterize_core(nc, p, f, 1)){
     return -1;
   }
 #define MIN_SUMODE_SIZE BUFSIZ
   if(*asu){
-    if(nc->rstate.mstrsize >= MIN_SUMODE_SIZE){
+    if(nc->rstate.f.used >= MIN_SUMODE_SIZE){
       const char* endasu = get_escape(&nc->tcache, ESCAPE_ESUM);
       if(endasu){
-        if(fprintf(out, "%s", endasu) < 0 || ncflush(out)){
+        if(fbuf_puts(f, endasu) < 0){
           *asu = 0;
         }
       }else{
@@ -1149,13 +1138,13 @@ notcurses_rasterize_inner(notcurses* nc, ncpile* p, FILE* out, unsigned* asu){
     }
   }
 #undef MIN_SUMODE_SIZE
-  return nc->rstate.mstrsize;
+  return nc->rstate.f.used;
 }
 
 // rasterize the rendered frame, and blockingly write it out to the terminal.
 static int
-raster_and_write(notcurses* nc, ncpile* p, FILE* out){
-  fseeko(out, 0, SEEK_SET);
+raster_and_write(notcurses* nc, ncpile* p, fbuf* f){
+  fbuf_reset(f);
   // will we be using application-synchronized updates? if this comes back as
   // non-zero, we are, and must emit the header. no SUM without a tty, and we
   // can't have the escape without being connected to one...
@@ -1164,11 +1153,11 @@ raster_and_write(notcurses* nc, ncpile* p, FILE* out){
   // if we have SUM support, emit a BSU speculatively. if we do so, but don't
   // actually use an ESU, this BSUM must be skipped on write.
   if(useasu){
-    if(ncfputs(basu, out) == EOF){
+    if(fbuf_puts(f, basu) < 0){
       return -1;
     }
   }
-  if(notcurses_rasterize_inner(nc, p, out, &useasu) < 0){
+  if(notcurses_rasterize_inner(nc, p, f, &useasu) < 0){
     return -1;
   }
   // if we loaded a BSU into the front, but don't actually want to use it,
@@ -1184,19 +1173,19 @@ raster_and_write(notcurses* nc, ncpile* p, FILE* out){
   int ret = 0;
   sigset_t oldmask;
   block_signals(&oldmask);
-  if(blocking_write(fileno(nc->ttyfp), nc->rstate.mstream + moffset,
-                    nc->rstate.mstrsize - moffset)){
+  if(blocking_write(fileno(nc->ttyfp), nc->rstate.f.buf + moffset,
+                    nc->rstate.f.used - moffset)){
     ret = -1;
   }
   unblock_signals(&oldmask);
 //fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu %d\n", nc->stats.defaultelisions, nc->stats.defaultemissions, nc->stats.fgelisions, nc->stats.fgemissions, nc->stats.bgelisions, nc->stats.bgemissions, ret);
   if(nc->renderfp){
-    fprintf(nc->renderfp, "%s\n", nc->rstate.mstream);
+    fprintf(nc->renderfp, "%s\n", (const char*)nc->rstate.f.buf);
   }
   if(ret < 0){
     return ret;
   }
-  return nc->rstate.mstrsize;
+  return nc->rstate.f.used;
 }
 
 // if the cursor is enabled, store its location and disable it. then, once done
@@ -1204,17 +1193,17 @@ raster_and_write(notcurses* nc, ncpile* p, FILE* out){
 // during rasterization, we'll get grotesque flicker. 'out' is a memstream
 // used to collect a buffer.
 static inline int
-notcurses_rasterize(notcurses* nc, ncpile* p, FILE* out){
+notcurses_rasterize(notcurses* nc, ncpile* p, fbuf* f){
   const int cursory = nc->cursory;
   const int cursorx = nc->cursorx;
   if(cursory >= 0){ // either both are good, or neither is
     notcurses_cursor_disable(nc);
   }
-  int ret = raster_and_write(nc, p, out);
+  int ret = raster_and_write(nc, p, f);
   if(cursory >= 0){
     notcurses_cursor_enable(nc, cursory, cursorx);
   }else if(nc->rstate.logendy >= 0){
-    goto_location(nc, nc->ttyfp, nc->rstate.logendy, nc->rstate.logendx);
+    goto_location(nc, f, nc->rstate.logendy, nc->rstate.logendx);
     fflush(nc->ttyfp);
   }
   nc->last_pile = p;
@@ -1223,33 +1212,28 @@ notcurses_rasterize(notcurses* nc, ncpile* p, FILE* out){
 
 // get the cursor to the upper-left corner by one means or another, clearing
 // the screen while doing so.
-int clear_and_home(notcurses* nc, tinfo* ti, FILE* fp, unsigned flush){
+int clear_and_home(notcurses* nc, tinfo* ti, fbuf* f){
   // clear clears the screen and homes the cursor by itself
   const char* clearscr = get_escape(ti, ESCAPE_CLEAR);
   if(clearscr){
-    if(term_emit(clearscr, fp, flush) == 0){
+    if(fbuf_emit(f, clearscr) == 0){
       goto success;
     }
   }
   const ncplane* stdn = notcurses_stdplane_const(nc);
   // clearscr didn't fly. try scrolling everything off. first, go to the
   // bottom of the screen, then write N newlines.
-  if(goto_location(nc, fp, ncplane_dim_y(stdn) - 1, 0)){
+  if(goto_location(nc, f, ncplane_dim_y(stdn) - 1, 0)){
     return -1;
   }
   for(int y = 0 ; y < ncplane_dim_y(stdn) ; ++y){
-    if(ncfputc('\n', fp) == EOF){
+    if(fbuf_putc(f, '\n') < 0){
       return -1;
     }
   }
-  if(goto_location(nc, fp, 0, 0)){
+  if(goto_location(nc, f, 0, 0)){
     return -1;
   }
-  if(flush){
-    if(ncflush(fp)){
-      return -1;
-    }
-  }
 
 success:
   nc->rstate.x = 0;
@@ -1262,7 +1246,8 @@ int notcurses_refresh(notcurses* nc, int* restrict dimy, int* restrict dimx){
   if(notcurses_resize(nc, dimy, dimx)){
     return -1;
   }
-  if(clear_and_home(nc, &nc->tcache, nc->ttyfp, true)){
+  fbuf_reset(&nc->rstate.f);
+  if(clear_and_home(nc, &nc->tcache, &nc->rstate.f)){
     return -1;
   }
   if(nc->lfdimx == 0 || nc->lfdimy == 0){
@@ -1280,11 +1265,18 @@ int notcurses_refresh(notcurses* nc, int* restrict dimy, int* restrict dimx){
   for(int i = 0 ; i < count ; ++i){
     p.crender[i].s.damaged = 1;
   }
-  int ret = notcurses_rasterize(nc, &p, nc->rstate.mstreamfp);
+  int ret = notcurses_rasterize(nc, &p, &nc->rstate.f);
   free(p.crender);
   if(ret < 0){
     return -1;
   }
+  if(nc->rstate.f.used){
+    fwrite(nc->rstate.f.buf, nc->rstate.f.used, 1, nc->ttyfp);
+    fbuf_reset(&nc->rstate.f);
+    if(ncflush(nc->ttyfp)){
+      return -1;
+    }
+  }
   ++nc->stats.s.refreshes;
   return 0;
 }
@@ -1295,35 +1287,31 @@ int ncpile_render_to_file(ncplane* n, FILE* fp){
   if(nc->lfdimx == 0 || nc->lfdimy == 0){
     return 0;
   }
-  char* rastered = NULL;
-  size_t rastbytes = 0;
-  FILE* out = open_memstream(&rastered, &rastbytes);
-  if(out == NULL){
+  fbuf f = {};
+  if(fbuf_init(&f)){
     return -1;
   }
   const int count = (nc->lfdimx > p->dimx ? nc->lfdimx : p->dimx) *
                     (nc->lfdimy > p->dimy ? nc->lfdimy : p->dimy);
   p->crender = malloc(count * sizeof(*p->crender));
   if(p->crender == NULL){
-    fclose(out);
-    free(rastered);
+    fbuf_free(&f);
     return -1;
   }
   init_rvec(p->crender, count);
   for(int i = 0 ; i < count ; ++i){
     p->crender[i].s.damaged = 1;
   }
-  int ret = raster_and_write(nc, p, out);
+  int ret = raster_and_write(nc, p, &f);
   free(p->crender);
   if(ret > 0){
-    if(fprintf(fp, "%s", rastered) == ret){
+    if(fwrite(f.buf, f.used, 1, fp) == 1){
       ret = 0;
     }else{
       ret = -1;
     }
   }
-  fclose(out);
-  free(rastered);
+  fbuf_free(&f);
   return ret;
 }
 
@@ -1370,7 +1358,7 @@ int ncpile_rasterize(ncplane* n){
   const struct tinfo* ti = &ncplane_notcurses_const(n)->tcache;
   postpaint(ti, nc->lastframe, miny, minx, pile->crender, &nc->pool);
   clock_gettime(CLOCK_MONOTONIC, &rasterdone);
-  int bytes = notcurses_rasterize(nc, pile, nc->rstate.mstreamfp);
+  int bytes = notcurses_rasterize(nc, pile, &nc->rstate.f);
   // accepts -1 as an indication of failure
   clock_gettime(CLOCK_MONOTONIC, &writedone);
   pthread_mutex_lock(&nc->stats.lock);
@@ -1445,19 +1433,19 @@ int ncpile_render_to_buffer(ncplane* p, char** buf, size_t* buflen){
   }
   notcurses* nc = ncplane_notcurses(p);
   unsigned useasu = false; // no SUM with file
-  fseeko(nc->rstate.mstreamfp, 0, SEEK_SET);
-  int bytes = notcurses_rasterize_inner(nc, ncplane_pile(p), nc->rstate.mstreamfp, &useasu);
+  fbuf_reset(&nc->rstate.f);
+  int bytes = notcurses_rasterize_inner(nc, ncplane_pile(p), &nc->rstate.f, &useasu);
   pthread_mutex_lock(&nc->stats.lock);
     update_render_bytes(&nc->stats.s, bytes);
   pthread_mutex_unlock(&nc->stats.lock);
   if(bytes < 0){
     return -1;
   }
-  *buf = memdup(nc->rstate.mstreamfp, nc->rstate.mstrsize);
+  *buf = memdup(nc->rstate.f.buf, nc->rstate.f.used);
   if(buf == NULL){
     return -1;
   }
-  *buflen = nc->rstate.mstrsize;
+  *buflen = nc->rstate.f.used;
   return 0;
 }
 
@@ -1506,9 +1494,19 @@ int ncdirect_set_bg_rgb(ncdirect* nc, unsigned rgb){
      && ncchannels_bg_rgb(nc->channels) == rgb){
     return 0;
   }
-  if(term_bg_rgb8(&nc->tcache, nc->ttyfp, (rgb & 0xff0000u) >> 16u, (rgb & 0xff00u) >> 8u, rgb & 0xffu)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
+  if(term_bg_rgb8(&nc->tcache, &f, (rgb & 0xff0000u) >> 16u, (rgb & 0xff00u) >> 8u, rgb & 0xffu)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fwrite(f.buf, f.used, 1, nc->ttyfp) != 1){
+    fbuf_free(&f);
     return -1;
   }
+  fbuf_free(&f);
   ncchannels_set_bg_rgb(&nc->channels, rgb);
   return 0;
 }
@@ -1521,9 +1519,19 @@ int ncdirect_set_fg_rgb(ncdirect* nc, unsigned rgb){
      && ncchannels_fg_rgb(nc->channels) == rgb){
     return 0;
   }
-  if(term_fg_rgb8(&nc->tcache, nc->ttyfp, (rgb & 0xff0000u) >> 16u, (rgb & 0xff00u) >> 8u, rgb & 0xffu)){
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
     return -1;
   }
+  if(term_fg_rgb8(&nc->tcache, &f, (rgb & 0xff0000u) >> 16u, (rgb & 0xff00u) >> 8u, rgb & 0xffu)){
+    fbuf_free(&f);
+    return -1;
+  }
+  if(fwrite(f.buf, f.used, 1, nc->ttyfp) != 1){
+    fbuf_free(&f);
+    return -1;
+  }
+  fbuf_free(&f);
   ncchannels_set_fg_rgb(&nc->channels, rgb);
   return 0;
 }
@@ -1547,13 +1555,20 @@ int notcurses_cursor_enable(notcurses* nc, int y, int x){
   if(nc->ttyfd < 0){
     return -1;
   }
+  fbuf f = {};
+  if(fbuf_init_small(&f)){
+    return -1;
+  }
   // updates nc->rstate.cursor{y,x}
-  if(goto_location(nc, nc->ttyfp, y + nc->margin_t, x + nc->margin_l)){
+  if(goto_location(nc, &f, y + nc->margin_t, x + nc->margin_l)){
+    fbuf_free(&f);
     return -1;
   }
-  if(ncflush(nc->ttyfp)){
+  if(fwrite(f.buf, f.used, 1, nc->ttyfp) != 1 || ncflush(nc->ttyfp)){
+    fbuf_free(&f);
     return -1;
   }
+  fbuf_free(&f);
   // if we were already positive, we're already visible, no need to write cnorm
   if(nc->cursory >= 0 && nc->cursorx >= 0){
     nc->cursory = y;
diff --git a/src/lib/signal.c b/src/lib/signal.c
index dcf20cec3c..d4c8ed2b4b 100644
--- a/src/lib/signal.c
+++ b/src/lib/signal.c
@@ -3,12 +3,50 @@
 #include <stdatomic.h>
 #include "internal.h"
 
+// primarily drive ownership off an atomic, safely used within a signal handler
+static void* _Atomic signal_nc = ATOMIC_VAR_INIT(NULL);
+
+#ifdef __MINGW64__
+int block_signals(sigset_t* old_blocked_signals){
+  (void)old_blocked_signals;
+  return 0;
+}
+
+int unblock_signals(const sigset_t* old_blocked_signals){
+  (void)old_blocked_signals;
+  return 0;
+}
+
+int drop_signals(void* nc){
+  void* expected = nc;
+  if(atomic_compare_exchange_strong(&signal_nc, &expected, nc)){
+    return -1;
+  }
+  return 0;
+}
+
+// this both sets up our signal handlers (unless that behavior has been
+// inhibited), and ensures that only one notcurses/ncdirect context is active
+// at any given time.
+int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sigs,
+                  int(*handler)(void*)){
+  (void)no_quit_sigs;
+  (void)no_winch_sigs;
+  (void)handler;
+  void* expected = NULL;
+  // don't register ourselves if we don't intend to set up signal handlers
+  // we expect NULL (nothing registered), and want to register nc
+  if(!atomic_compare_exchange_strong(&signal_nc, &expected, vnc)){
+    loginfo("%p is already registered for signals (provided %p)\n", expected, vnc);
+    return -1;
+  }
+  return 0;
+}
+#else
 // only one notcurses object can be the target of signal handlers, due to their
 // process-wide nature. hold this lock over any of the shared data below.
 static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
-// we still need an atomic signal_nc, despite guarding with the mutex, so that
-// it can be safely looked up within the signal handler.
-static void* _Atomic signal_nc = ATOMIC_VAR_INIT(NULL); // ugh
+
 static bool handling_winch;
 static bool handling_fatals;
 
@@ -107,7 +145,7 @@ fatal_handler(int signo, siginfo_t* siginfo, void* v){
 // this both sets up our signal handlers (unless that behavior has been
 // inhibited), and ensures that only one notcurses/ncdirect context is active
 // at any given time.
-int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sig,
+int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sigs,
                   int(*handler)(void*)){
   notcurses* nc = vnc;
   void* expected = NULL;
@@ -118,7 +156,8 @@ int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sig,
     loginfo("%p is already registered for signals (provided %p)\n", expected, nc);
     return -1;
   }
-  if(!no_winch_sig){
+  pthread_mutex_lock(&lock);
+  if(!no_winch_sigs){
     memset(&sa, 0, sizeof(sa));
     sa.sa_handler = sigwinch_handler;
     sigaddset(&sa.sa_mask, SIGWINCH);
@@ -128,6 +167,7 @@ int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sig,
     ret |= sigaction(SIGCONT, &sa, &old_cont);
     if(ret){
       atomic_store(&signal_nc, NULL);
+      pthread_mutex_unlock(&lock);
       fprintf(stderr, "Error installing term signal handler (%s)\n", strerror(errno));
       return -1;
     }
@@ -155,6 +195,7 @@ int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sig,
     ret |= sigaction(SIGTERM, &sa, &old_term);
     if(ret){
       atomic_store(&signal_nc, NULL);
+      pthread_mutex_unlock(&lock);
       fprintf(stderr, "Error installing fatal signal handlers (%s)\n", strerror(errno));
       return -1;
     }
@@ -165,5 +206,7 @@ int setup_signals(void* vnc, bool no_quit_sigs, bool no_winch_sig,
   sigaddset(&wblock_signals, SIGINT);
   sigaddset(&wblock_signals, SIGTERM);
   sigaddset(&wblock_signals, SIGQUIT);
+  pthread_mutex_unlock(&lock);
   return 0;
 }
+#endif
diff --git a/src/lib/sixel.c b/src/lib/sixel.c
index 139f840ace..b862a4e668 100644
--- a/src/lib/sixel.c
+++ b/src/lib/sixel.c
@@ -1,4 +1,5 @@
 #include "internal.h"
+#include "fbuf.h"
 
 #define RGBSIZE 3
 #define CENTSIZE (RGBSIZE + 1) // size of a color table entry
@@ -237,7 +238,7 @@ int sixel_wipe(sprixel* s, int ycell, int xcell){
   if(w){
     s->wipes_outstanding = true;
   }
-  change_p2(s->glyph, SIXEL_P2_TRANS);
+  change_p2(s->glyph.buf, SIXEL_P2_TRANS);
   s->n->tam[s->dimx * ycell + xcell].auxvector = auxvec;
   return 0;
 }
@@ -535,40 +536,130 @@ refine_color_table(const uint32_t* data, int linesize, int begy, int begx,
 // Emit some number of equivalent, subsequent sixels, using sixel RLE. We've
 // seen the sixel |crle| for |seenrle| columns in a row. |seenrle| must > 0.
 static int
-write_rle(int* printed, int color, FILE* fp, int seenrle, unsigned char crle,
+write_rle(int* printed, int color, fbuf* f, int seenrle, unsigned char crle,
           int* needclosure){
   if(!*printed){
-    fprintf(fp, "%s#%d", *needclosure ? "$" : "", color);
+    if(*needclosure){
+      if(fbuf_putc(f, '$') != 1){
+        return -1;
+      }
+    }
+    if(fbuf_putc(f, '#') != 1){
+      return -1;
+    }
+    if(fbuf_putint(f, color) < 0){
+      return -1;
+    }
     *printed = 1;
     *needclosure = 0;
   }
   crle += 63;
-  if(seenrle == 1){
-    if(fputc(crle, fp) == EOF){
+  if(seenrle == 2){
+    if(fbuf_putc(f, crle) != 1){
       return -1;
     }
-  }else if(seenrle == 2){
-    if(fprintf(fp, "%c%c", crle, crle) <= 0){
+  }else if(seenrle != 1){
+    if(fbuf_putc(f, '!') != 1){
       return -1;
     }
-  }else{
-    if(fprintf(fp, "!%d%c", seenrle, crle) <= 0){
+    if(fbuf_putint(f, seenrle) < 0){
       return -1;
     }
   }
+  if(fbuf_putc(f, crle) != 1){
+    return -1;
+  }
   return 0;
 }
 
+static inline int
+write_sixel_intro(fbuf* f, sixel_p2_e p2, int leny, int lenx){
+  int r = fbuf_puts(f, "\x1bP0;");
+  if(r < 0){
+    return -1;
+  }
+  int rr = fbuf_putint(f, p2);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  rr = fbuf_puts(f, ";0q\"1;1;");
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  rr = fbuf_putint(f, lenx);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  if(fbuf_putc(f, ';') != 1){
+    return -1;
+  }
+  ++r;
+  rr = fbuf_putint(f, leny);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  return r;
+}
+
+// write a single color register. rc/gc/bc are on [0..100].
+static inline int
+write_sixel_creg(fbuf* f, int idx, int rc, int gc, int bc){
+  int r = 0;
+  if(fbuf_putc(f, '#') != 1){
+    return -1;
+  }
+  ++r;
+  int rr = fbuf_putint(f, idx);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  rr = fbuf_puts(f, ";2;");
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  rr = fbuf_putint(f, rc);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  if(fbuf_putc(f, ';') != 1){
+    return -1;
+  }
+  ++r;
+  rr = fbuf_putint(f, gc);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  if(fbuf_putc(f, ';') != 1){
+    return -1;
+  }
+  ++r;
+  rr = fbuf_putint(f, bc);
+  if(rr < 0){
+    return -1;
+  }
+  r += rr;
+  return r;
+}
+
 // write the escape which opens a Sixel, plus the palette table. returns the
 // number of bytes written, so that this header can be directly copied in
 // future reencodings. |leny| and |lenx| are output pixel geometry.
+// returns the number of bytes written, so it can be stored at *parse_start.
 static int
-write_sixel_header(FILE* fp, int leny, int lenx, const sixeltable* stab, sixel_p2_e p2){
+write_sixel_header(fbuf* f, int leny, int lenx, const sixeltable* stab, sixel_p2_e p2){
   if(leny % 6){
     return -1;
   }
   // Set Raster Attributes - pan/pad=1 (pixel aspect ratio), Ph=lenx, Pv=leny
-  int r = fprintf(fp, "\eP0;%d;0q\"1;1;%d;%d", p2, lenx, leny);
+  int r = write_sixel_intro(f, p2, leny, lenx);
   if(r < 0){
     return -1;
   }
@@ -580,20 +671,19 @@ write_sixel_header(FILE* fp, int leny, int lenx, const sixeltable* stab, sixel_p
     //fprintf(fp, "#%d;2;%u;%u;%u", i, rgb[0], rgb[1], rgb[2]);
     // we emit the average of the actual sums rather than the RGB clustering
     // point, as it can be (and usually is) much more accurate.
-    int f = fprintf(fp, "#%d;2;%" PRId64 ";%" PRId64 ";%" PRId64, i,
-                    (stab->deets[idx].sums[0] * 100 / count / 255),
-                    (stab->deets[idx].sums[1] * 100 / count / 255),
-                    (stab->deets[idx].sums[2] * 100 / count / 255));
-    if(f < 0){
+    int rr = write_sixel_creg(f, i, (stab->deets[idx].sums[0] * 100 / count / 255),
+                              (stab->deets[idx].sums[1] * 100 / count / 255),
+                              (stab->deets[idx].sums[2] * 100 / count / 255));
+    if(rr < 0){
       return -1;
     }
-    r += f;
+    r += rr;
   }
   return r;
 }
 
 static int
-write_sixel_payload(FILE* fp, int lenx, const sixelmap* map){
+write_sixel_payload(fbuf* f, int lenx, const sixelmap* map){
   int p = 0;
   while(p < map->sixelcount){
     int needclosure = 0;
@@ -609,7 +699,9 @@ write_sixel_payload(FILE* fp, int lenx, const sixelmap* map){
           if(map->data[idx * map->sixelcount + m] == crle){
             ++seenrle;
           }else{
-            write_rle(&printed, i, fp, seenrle, crle, &needclosure);
+            if(write_rle(&printed, i, f, seenrle, crle, &needclosure)){
+              return -1;
+            }
             seenrle = 1;
             crle = map->data[idx * map->sixelcount + m];
           }
@@ -619,16 +711,22 @@ write_sixel_payload(FILE* fp, int lenx, const sixelmap* map){
         }
       }
       if(crle){
-        write_rle(&printed, i, fp, seenrle, crle, &needclosure);
+        if(write_rle(&printed, i, f, seenrle, crle, &needclosure)){
+          return -1;
+        }
       }
       needclosure = needclosure | printed;
     }
     if(p + lenx < map->sixelcount){
-      fputc('-', fp);
+      if(fbuf_putc(f, '-') != 1){
+        return -1;
+      }
     }
     p += lenx;
   }
-  fprintf(fp, "\e\\");
+  if(fbuf_puts(f, "\e\\") < 0){
+    return -1;
+  }
   return 0;
 }
 
@@ -637,16 +735,13 @@ write_sixel_payload(FILE* fp, int lenx, const sixelmap* map){
 // constant, and is simply copied. fclose()s |fp| on success. |outx| and |outy|
 // are output geometry.
 static int
-write_sixel(FILE* fp, int outy, int outx, const sixeltable* stab,
+write_sixel(fbuf* f, int outy, int outx, const sixeltable* stab,
             int* parse_start, sixel_p2_e p2){
-  *parse_start = write_sixel_header(fp, outy, outx, stab, p2);
+  *parse_start = write_sixel_header(f, outy, outx, stab, p2);
   if(*parse_start < 0){
     return -1;
   }
-  if(write_sixel_payload(fp, outx, stab->map) < 0){
-    return -1;
-  }
-  if(fclose(fp) == EOF){
+  if(write_sixel_payload(f, outx, stab->map) < 0){
     return -1;
   }
   return 0;
@@ -662,30 +757,21 @@ write_sixel(FILE* fp, int outy, int outx, const sixeltable* stab,
 // is redrawn, and annihilated sprixcells still require a glyph to be emitted.
 static inline int
 sixel_reblit(sprixel* s){
-  char* buf = NULL;
-  size_t size = 0;
-  FILE* fp = open_memstream(&buf, &size);
-  if(fp == NULL){
-    return -1;
-  }
-  if(fwrite(s->glyph, s->parse_start, 1, fp) != 1){
-    fclose(fp);
-    free(buf);
+  fbuf f;
+  if(fbuf_init(&f)){
     return -1;
   }
-  if(write_sixel_payload(fp, s->pixx, s->smap) < 0){
-    fclose(fp);
-    free(buf);
+  if(fbuf_putn(&f, s->glyph.buf, s->parse_start) != s->parse_start){
+    fbuf_free(&f);
     return -1;
   }
-  if(fclose(fp) == EOF){
-    free(buf);
+  if(write_sixel_payload(&f, s->pixx, s->smap) < 0){
+    fbuf_free(&f);
     return -1;
   }
-  free(s->glyph);
+  fbuf_free(&s->glyph);
   // FIXME update P2 if necessary
-  s->glyph = buf;
-  s->glyphlen = size;
+  memcpy(&s->glyph, &f, sizeof(f));
   return 0;
 }
 
@@ -697,10 +783,8 @@ sixel_reblit(sprixel* s){
 // transparent filler input for any missing rows.
 static inline int
 sixel_blit_inner(int leny, int lenx, sixeltable* stab, sprixel* s, tament* tam){
-  char* buf = NULL;
-  size_t size = 0;
-  FILE* fp = open_memstream(&buf, &size);
-  if(fp == NULL){
+  fbuf f;
+  if(fbuf_init(&f)){
     return -1;
   }
   int parse_start = 0;
@@ -710,17 +794,17 @@ sixel_blit_inner(int leny, int lenx, sixeltable* stab, sprixel* s, tament* tam){
     stab->p2 = SIXEL_P2_TRANS;
   }
   // calls fclose() on success
-  if(write_sixel(fp, outy, lenx, stab, &parse_start, stab->p2)){
-    fclose(fp);
-    free(buf);
+  if(write_sixel(&f, outy, lenx, stab, &parse_start, stab->p2)){
+    fbuf_free(&f);
     return -1;
   }
   scrub_tam_boundaries(tam, outy, lenx, s->cellpxy, s->cellpxx);
   // take ownership of buf on success
-  if(plane_blit_sixel(s, buf, size, outy, lenx, parse_start, tam) < 0){
-    free(buf);
+  if(plane_blit_sixel(s, &f, outy, lenx, parse_start, tam) < 0){
+    fbuf_free(&f);
     return -1;
   }
+  // we're keeping the buf remnants
   sixelmap_trim(stab->map);
   s->smap = stab->map;
   return 1;
@@ -832,7 +916,7 @@ int sixel_scrub(const ncpile* p, sprixel* s){
 }
 
 // returns the number of bytes written
-int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
+int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
                int y, int x){
   (void)ti;
   // if we've wiped or rebuilt any cells, effect those changes now, or else
@@ -844,7 +928,7 @@ int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
     s->wipes_outstanding = false;
   }
   if(p){
-    if(goto_location(p->nc, out, y, x)){
+    if(goto_location(p->nc, f, y, x)){
       return -1;
     }
     if(s->invalidated == SPRIXEL_MOVED){
@@ -858,11 +942,11 @@ int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, FILE* out,
       }
     }
   }
-  if(fwrite(s->glyph, s->glyphlen, 1, out) != 1){
+  if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
     return -1;
   }
   s->invalidated = SPRIXEL_QUIESCENT;
-  return s->glyphlen;
+  return s->glyph.used;
 }
 
 int sixel_init(const tinfo* ti, int fd){
@@ -931,8 +1015,8 @@ int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 
 // 80 (sixel scrolling) is enabled by default. 8452 is not. XTSAVE/XTRESTORE
 // would be better, where they're supported.
-int sixel_shutdown(FILE* fp){
-  return term_emit("\e[?8452l", fp, false);
+int sixel_shutdown(fbuf* f){
+  return fbuf_emit(f, "\e[?8452l");
 }
 
 uint8_t* sixel_trans_auxvec(const tinfo* ti){
diff --git a/src/lib/sprite.c b/src/lib/sprite.c
index 9919fd004f..78ade957ab 100644
--- a/src/lib/sprite.c
+++ b/src/lib/sprite.c
@@ -5,8 +5,8 @@
 static atomic_uint_fast32_t sprixelid_nonce;
 
 void sprixel_debug(const sprixel* s, FILE* out){
-  fprintf(out, "Sprixel %d (%p) %zuB %dx%d (%dx%d) @%d/%d state: %d\n",
-          s->id, s, s->glyphlen, s->dimy, s->dimx, s->pixy, s->pixx,
+  fprintf(out, "Sprixel %d (%p) %" PRIu64 "B %dx%d (%dx%d) @%d/%d state: %d\n",
+          s->id, s, s->glyph.used, s->dimy, s->dimx, s->pixy, s->pixx,
           s->n ? s->n->absy : 0, s->n ? s->n->absx : 0,
           s->invalidated);
   if(s->n){
@@ -42,14 +42,11 @@ void sprixel_debug(const sprixel* s, FILE* out){
 void sprixel_free(sprixel* s){
   if(s){
     loginfo("Destroying sprixel %u\n", s->id);
-    if(s->mstreamfp){
-      fclose(s->mstreamfp);
-    }
     if(s->n){
       s->n->sprite = NULL;
     }
     sixelmap_free(s->smap);
-    free(s->glyph);
+    fbuf_free(&s->glyph);
     free(s);
   }
 }
@@ -120,30 +117,35 @@ void sprixel_invalidate(sprixel* s, int y, int x){
 
 sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx){
   sprixel* ret = malloc(sizeof(sprixel));
-  if(ret){
-    memset(ret, 0, sizeof(*ret));
-    ret->n = n;
-    ret->dimy = dimy;
-    ret->dimx = dimx;
-    ret->id = ++sprixelid_nonce;
-    if(ret->id >= 0x1000000){
-      ret->id = 1;
-      sprixelid_nonce = 1;
-    }
+  if(ret == NULL){
+    return NULL;
+  }
+  memset(ret, 0, sizeof(*ret));
+  if(fbuf_init(&ret->glyph)){
+    free(ret);
+    return NULL;
+  }
+  ret->n = n;
+  ret->dimy = dimy;
+  ret->dimx = dimx;
+  ret->id = ++sprixelid_nonce;
+  if(ret->id >= 0x1000000){
+    ret->id = 1;
+    sprixelid_nonce = 1;
+  }
 //fprintf(stderr, "LOOKING AT %p (p->n = %p)\n", ret, ret->n);
-    ret->cellpxy = ti->cellpixy;
-    ret->cellpxx = ti->cellpixx;
-    if(ncplane_pile(ret->n)){ // rendered mode
-      ncpile* np = ncplane_pile(ret->n);
-      if( (ret->next = np->sprixelcache) ){
-        ret->next->prev = ret;
-      }
-      np->sprixelcache = ret;
-      ret->prev = NULL;
-//fprintf(stderr, "%p %p %p\n", nc->sprixelcache, ret, nc->sprixelcache->next);
-    }else{ // ncdirect case
-      ret->next = ret->prev = NULL;
+  ret->cellpxy = ti->cellpixy;
+  ret->cellpxx = ti->cellpixx;
+  if(ncplane_pile(ret->n)){ // rendered mode
+    ncpile* np = ncplane_pile(ret->n);
+    if( (ret->next = np->sprixelcache) ){
+      ret->next->prev = ret;
     }
+    np->sprixelcache = ret;
+    ret->prev = NULL;
+//fprintf(stderr, "%p %p %p\n", nc->sprixelcache, ret, nc->sprixelcache->next);
+  }else{ // ncdirect case
+    ret->next = ret->prev = NULL;
   }
   return ret;
 }
@@ -151,7 +153,7 @@ sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx){
 // |pixy| and |pixx| are the output pixel geometry (i.e. |pixy| must be a
 // multiple of 6 for sixel). output coverage ought already have been loaded.
 // takes ownership of 's' on success. frees any existing glyph.
-int sprixel_load(sprixel* spx, char* s, int bytes, int pixy, int pixx,
+int sprixel_load(sprixel* spx, fbuf* f, int pixy, int pixx,
                  int parse_start){
   assert(spx->n);
   if(spx->cellpxy > 0){ // don't explode on ncdirect case
@@ -162,11 +164,10 @@ int sprixel_load(sprixel* spx, char* s, int bytes, int pixy, int pixx,
       return -1;
     }
   }
-  if(spx->glyph != s){
-    free(spx->glyph);
-    spx->glyph = s;
+  if(&spx->glyph != f){
+    fbuf_free(&spx->glyph);
+    memcpy(&spx->glyph, f, sizeof(*f));
   }
-  spx->glyphlen = bytes;
   spx->invalidated = SPRIXEL_INVALIDATED;
   spx->pixx = pixx;
   spx->pixy = pixy;
@@ -206,11 +207,11 @@ int sprite_wipe(const notcurses* nc, sprixel* s, int ycell, int xcell){
   return r;
 }
 
-int sprite_clear_all(const tinfo* t, FILE* fp){
+int sprite_clear_all(const tinfo* t, fbuf* f){
   if(t->pixel_clear_all == NULL){
     return 0;
   }
-  return t->pixel_clear_all(fp);
+  return t->pixel_clear_all(f);
 }
 
 int sprite_init(const tinfo* t, int fd){
diff --git a/src/lib/sprite.h b/src/lib/sprite.h
index d5b665a8c0..bd352f75f5 100644
--- a/src/lib/sprite.h
+++ b/src/lib/sprite.h
@@ -7,6 +7,7 @@ extern "C" {
 
 #include <stdint.h>
 #include <stdbool.h>
+#include "fbuf.h"
 
 struct tinfo;
 struct ncpile;
@@ -131,8 +132,7 @@ typedef struct tament {
 // protocol, we just have to rewrite them. there's a doubly-linked list of
 // sprixels per ncpile, to which the pile keeps a head link.
 typedef struct sprixel {
-  char* glyph;          // glyph; can be quite large
-  size_t glyphlen;      // length of the glyph in bytes
+  fbuf glyph;
   uint32_t id;          // embedded into gcluster field of nccell, 24 bits
   // both the plane and visual can die before the sprixel does. they are
   // responsible in such a case for NULLing out this link themselves.
@@ -151,9 +151,8 @@ typedef struct sprixel {
   int parse_start;      // where to start parsing for cell wipes
   // only used for sixel-based sprixels
   struct sixelmap* smap;  // copy of palette indices + transparency bits
-  FILE* mstreamfp;        // mstream for writing animation-type updates,
-                          // (only available for kitty animation sprixels)
   bool wipes_outstanding; // do we need rebuild the sixel next render?
+  bool animating;        // do we have an active animation?
 } sprixel;
 
 int sixel_wipe(sprixel* s, int ycell, int xcell);
@@ -175,24 +174,24 @@ int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int kitty_rebuild_selfref(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int sixel_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
-               FILE* out, int y, int x);
+               fbuf* f, int y, int x);
 int kitty_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
-               FILE* out, int y, int x);
+               fbuf* f, int y, int x);
 int iterm_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
-               FILE* out, int y, int x);
-int kitty_move(sprixel* s, FILE* out, unsigned noscroll);
+               fbuf* f, int y, int x);
+int kitty_move(sprixel* s, fbuf* f, unsigned noscroll);
 int sixel_scrub(const struct ncpile* p, sprixel* s);
 int kitty_scrub(const struct ncpile* p, sprixel* s);
 int fbcon_scrub(const struct ncpile* p, sprixel* s);
-int kitty_remove(int id, FILE* out);
-int kitty_clear_all(FILE* fp);
+int kitty_remove(int id, fbuf* f);
+int kitty_clear_all(fbuf* f);
 int sixel_init(const tinfo* t, int fd);
 int sixel_init_inverted(const tinfo* t, int fd);
-int kitty_shutdown(FILE* fp);
-int sixel_shutdown(FILE* fp);
+int kitty_shutdown(fbuf* f);
+int sixel_shutdown(fbuf* f);
 uint8_t* sixel_trans_auxvec(const struct tinfo* ti);
 uint8_t* kitty_trans_auxvec(const struct tinfo* ti);
-int kitty_commit(FILE* fp, sprixel* s, unsigned noscroll);
+int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll);
 int sixel_blit(struct ncplane* nc, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs);
 int kitty_blit(struct ncplane* nc, int linesize, const void* data,
@@ -206,7 +205,7 @@ int kitty_blit_selfref(struct ncplane* nc, int linesize, const void* data,
 int fbcon_blit(struct ncplane* nc, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs);
 int fbcon_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
-               FILE* out, int y, int x);
+               fbuf* f, int y, int x);
 
 typedef enum {
   // C=1 (disabling scrolling) was only introduced in 0.20.0, at the same
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index a511eca3da..ae80c9db7e 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -4,6 +4,7 @@
 #include <sys/utsname.h>
 #endif
 #include "internal.h"
+#include "windows.h"
 #include "input.h"
 #include "linux.h"
 
@@ -149,47 +150,6 @@ query_rgb(void){
   return rgb;
 }
 
-static int
-terminfostr(char** gseq, const char* name){
-  *gseq = tigetstr(name);
-  if(*gseq == NULL || *gseq == (char*)-1){
-    *gseq = NULL;
-    return -1;
-  }
-  // terminfo syntax allows a number N of milliseconds worth of pause to be
-  // specified using $<N> syntax. this is then honored by tputs(). but we don't
-  // use tputs(), instead preferring the much faster stdio+tiparm() (at the
-  // expense of terminals which do require these delays). to avoid dumping
-  // "$<N>" sequences all over stdio, we chop them out. real text can follow
-  // them, so we continue on, copying back once out of the delay.
-  char* wnext = NULL; // NULL until we hit a delay, then place to write
-  bool indelay = false; // true iff we're in a delay section
-  // we consider it a delay as soon as we see '$', and the delay ends at '>'
-  for(char* cur = *gseq ; *cur ; ++cur){
-    if(!indelay){
-      // if we're not in a delay section, make sure we're not starting one,
-      // and otherwise copy the current character back (if necessary).
-      if(*cur == '$'){
-        wnext = cur;
-        indelay = true;
-      }else{
-        if(wnext){
-          *wnext++ = *cur;
-        }
-      }
-    }else{
-      // we are in a delay section. make sure we're not ending one.
-      if(*cur == '>'){
-        indelay = false;
-      }
-    }
-  }
-  if(wnext){
-    *wnext = '\0';
-  }
-  return 0;
-}
-
 // we couldn't get a terminal from interrogation, so let's see if the TERM
 // matches any of our known terminals. this can only be as accurate as the
 // TERM setting is (and as up-to-date and complete as we are).
@@ -260,6 +220,64 @@ compare_versions(const char* restrict v1, const char* restrict v2){
   return 0;
 }
 
+static inline int
+terminfostr(char** gseq, const char* name){
+  *gseq = tigetstr(name);
+  if(*gseq == NULL || *gseq == (char*)-1){
+    *gseq = NULL;
+    return -1;
+  }
+  // terminfo syntax allows a number N of milliseconds worth of pause to be
+  // specified using $<N> syntax. this is then honored by tputs(). but we don't
+  // use tputs(), instead preferring the much faster stdio+tiparm() (at the
+  // expense of terminals which do require these delays). to avoid dumping
+  // "$<N>" sequences all over stdio, we chop them out. real text can follow
+  // them, so we continue on, copying back once out of the delay.
+  char* wnext = NULL; // NULL until we hit a delay, then place to write
+  bool indelay = false; // true iff we're in a delay section
+  // we consider it a delay as soon as we see '$', and the delay ends at '>'
+  for(char* cur = *gseq ; *cur ; ++cur){
+    if(!indelay){
+      // if we're not in a delay section, make sure we're not starting one,
+      // and otherwise copy the current character back (if necessary).
+      if(*cur == '$'){
+        wnext = cur;
+        indelay = true;
+      }else{
+        if(wnext){
+          *wnext++ = *cur;
+        }
+      }
+    }else{
+      // we are in a delay section. make sure we're not ending one.
+      if(*cur == '>'){
+        indelay = false;
+      }
+    }
+  }
+  if(wnext){
+    *wnext = '\0';
+  }
+  return 0;
+}
+
+static inline int
+init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
+                  size_t* tablelen, size_t* tableused){
+  char* tstr;
+  if(ti->escindices[idx]){
+    return 0;
+  }
+  if(terminfostr(&tstr, name) == 0){
+    if(grow_esc_table(ti, tstr, idx, tablelen, tableused)){
+      return -1;
+    }
+  }else{
+    ti->escindices[idx] = 0;
+  }
+  return 0;
+}
+
 // Tertiary Device Attributes, necessary to identify VTE.
 // https://vt100.net/docs/vt510-rm/DA3.html
 // Replies with DCS ! | ... ST
@@ -354,20 +372,6 @@ send_initial_queries(int fd, bool minimal){
   return 0;
 }
 
-static int
-init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
-                  size_t* tablelen, size_t* tableused){
-  char* tstr;
-  if(terminfostr(&tstr, name) == 0){
-    if(grow_esc_table(ti, tstr, idx, tablelen, tableused)){
-      return -1;
-    }
-  }else{
-    ti->escindices[idx] = 0;
-  }
-  return 0;
-}
-
 // if we get a response to the standard cursor locator escape, we know this
 // terminal supports it, hah.
 static int
@@ -666,9 +670,18 @@ int interrogate_terminfo(tinfo* ti, int fd, unsigned utf8, unsigned noaltscreen,
   const char* tname = termname(); // longname() is also available
   queried_terminals_e qterm = TERMINAL_UNKNOWN;
   memset(ti, 0, sizeof(*ti));
+  size_t tablelen = 0;
+  size_t tableused = 0;
 #ifdef __APPLE__
   qterm = macos_early_matches(tname);
-#endif
+  (void)nonewfonts;
+#elif defined(__MINGW64__)
+  if(prepare_windows_terminal(ti, &tablelen, &tableused)){
+    return -1;
+  }
+  qterm = TERMINAL_MSTERMINAL;
+  (void)nonewfonts;
+#elif defined(__linux__)
   ti->linux_fb_fd = -1;
   ti->linux_fbuffer = MAP_FAILED;
   // we might or might not program quadrants into the console font
@@ -678,7 +691,11 @@ int interrogate_terminfo(tinfo* ti, int fd, unsigned utf8, unsigned noaltscreen,
       // FIXME set up pixel-drawing API for framebuffer #1369
     }
   }
+#else
+  (void)nonewfonts;
+#endif
   if(fd >= 0){
+#ifndef __MINGW64__
     if(tcgetattr(fd, &ti->tpreserved)){
       fprintf(stderr, "Couldn't preserve terminal state for %d (%s)\n", fd, strerror(errno));
       return -1;
@@ -695,6 +712,7 @@ int interrogate_terminfo(tinfo* ti, int fd, unsigned utf8, unsigned noaltscreen,
       fprintf(stderr, "Error issuing terminal queries on %d\n", fd);
       return -1;
     }
+#endif
   }
   ti->sprixel_scale_height = 1;
   get_default_geometry(ti);
@@ -747,8 +765,6 @@ int interrogate_terminfo(tinfo* ti, int fd, unsigned utf8, unsigned noaltscreen,
     { ESCAPE_U7, "u7", },
     { ESCAPE_MAX, NULL, },
   };
-  size_t tablelen = 0;
-  size_t tableused = 0;
   for(typeof(*strtdescs)* strtdesc = strtdescs ; strtdesc->esc < ESCAPE_MAX ; ++strtdesc){
     if(init_terminfo_esc(ti, strtdesc->tinfo, strtdesc->esc, &tablelen, &tableused)){
       goto err;
@@ -777,11 +793,11 @@ int interrogate_terminfo(tinfo* ti, int fd, unsigned utf8, unsigned noaltscreen,
     ti->escindices[ESCAPE_SMCUP] = 0;
     ti->escindices[ESCAPE_RMCUP] = 0;
   }
-  // ensure that the terminal provides automatic margins
+  /* // ensure that the terminal provides automatic margins
   if(tigetflag("am") != 1){
     fprintf(stderr, "Required terminfo capability 'am' not defined\n");
     goto err;
-  }
+  } */
   if(get_escape(ti, ESCAPE_CIVIS) == NULL){
     char* chts;
     if(terminfostr(&chts, "chts") == 0){
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index c051c0b20e..4c3592b148 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -12,6 +12,7 @@ extern "C" {
 #include <pthread.h>
 #include <stdbool.h>
 #include <notcurses/notcurses.h>
+#include "fbuf.h"
 
 struct ncpile;
 struct sprixel;
@@ -150,17 +151,17 @@ typedef struct tinfo {
   int (*pixel_wipe)(struct sprixel* s, int y, int x);
   // perform the inverse of pixel_wipe, restoring an annihilated sprixcell.
   int (*pixel_rebuild)(struct sprixel* s, int y, int x, uint8_t* auxvec);
-  int (*pixel_remove)(int id, FILE* out); // kitty only, issue actual delete command
+  int (*pixel_remove)(int id, fbuf* f); // kitty only, issue actual delete command
   int (*pixel_init)(const struct tinfo*, int fd); // called when support is detected
   int (*pixel_draw)(const struct tinfo*, const struct ncpile* p,
-                    struct sprixel* s, FILE* out, int y, int x);
+                    struct sprixel* s, fbuf* f, int y, int x);
   // execute move (erase old graphic, place at new location) if non-NULL
-  int (*pixel_move)(struct sprixel* s, FILE* out, unsigned noscroll);
+  int (*pixel_move)(struct sprixel* s, fbuf* f, unsigned noscroll);
   int (*pixel_scrub)(const struct ncpile* p, struct sprixel* s);
-  int (*pixel_shutdown)(FILE* fp);  // called during context shutdown
-  int (*pixel_clear_all)(FILE* fp); // called during context startup
+  int (*pixel_shutdown)(fbuf* f);   // called during context shutdown
+  int (*pixel_clear_all)(fbuf* f);  // called during context startup
   // make a loaded graphic visible. only used with kitty.
-  int (*pixel_commit)(FILE* fp, struct sprixel* s, unsigned noscroll);
+  int (*pixel_commit)(fbuf* f, struct sprixel* s, unsigned noscroll);
   uint8_t* (*pixel_trans_auxvec)(const struct tinfo* ti); // create tranparent auxvec
   // sprixel parameters. there are several different sprixel protocols, of
   // which we support sixel and kitty. the kitty protocol is used based
@@ -179,16 +180,20 @@ typedef struct tinfo {
   int sprixel_scale_height; // sprixel must be a multiple of this many rows
   const char* termname;     // terminal name from environment variables/init
   char* termversion;        // terminal version (freeform) from query responses
+#ifndef __MINGW64__
   struct termios tpreserved;// terminal state upon entry
+#endif
   ncinputlayer input;       // input layer
 
   int default_rows; // LINES environment var / lines terminfo / 24
   int default_cols; // COLUMNS environment var / cols terminfo / 80
 
+#ifdef __linux__
   int linux_fb_fd;        // linux framebuffer device fd
   char* linux_fb_dev;     // device corresponding to linux_fb_dev
   uint8_t* linux_fbuffer; // mmap()ed framebuffer
   size_t linux_fb_len;    // size of map
+#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.
diff --git a/src/lib/aux.c b/src/lib/util.c
similarity index 97%
rename from src/lib/aux.c
rename to src/lib/util.c
index ec2dd504a1..2e4bcbea95 100644
--- a/src/lib/aux.c
+++ b/src/lib/util.c
@@ -1,6 +1,8 @@
 #ifndef __MINGW64__
 #include <pwd.h>
 #include <unistd.h>
+#else
+#include <winsock2.h>
 #endif
 #include "internal.h"
 
diff --git a/src/lib/visual.c b/src/lib/visual.c
index b835bb320b..7c4d60059c 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -24,9 +24,9 @@ int ncvisual_init(int logl){
   return 0;
 }
 
-void ncvisual_printbanner(const notcurses* nc){
+void ncvisual_printbanner(fbuf* f){
   if(visual_implementation.visual_printbanner){
-    visual_implementation.visual_printbanner(nc);
+    visual_implementation.visual_printbanner(f);
   }
 }
 
diff --git a/src/lib/windows.c b/src/lib/windows.c
index 54d45d167a..bcd2946487 100644
--- a/src/lib/windows.c
+++ b/src/lib/windows.c
@@ -43,5 +43,6 @@ int prepare_windows_terminal(tinfo* ti, size_t* tablelen, size_t* tableused){
   ti->caps.colors = 256;
   ti->caps.quadrants = true;
   ti->caps.braille = true;
+  ti->termname = "Windows Terminal";
   return 0;
 }
diff --git a/src/ls/main.cpp b/src/ls/main.cpp
index c8946183cf..275edcefb1 100644
--- a/src/ls/main.cpp
+++ b/src/ls/main.cpp
@@ -82,6 +82,7 @@ int handle_inode(const std::string& dir, const char* p, const struct stat* st, c
 // passing false for toplevel (but preserving |ctx|).
 int handle_dir(int dirfd, const std::string& pdir, const char* p,
                const struct stat* st, const lsContext& ctx, bool toplevel){
+#ifndef __MINGW64__
   if(ctx.directories){
     return handle_inode(pdir, p, st, ctx);
   }
@@ -116,6 +117,9 @@ int handle_dir(int dirfd, const std::string& pdir, const char* p,
   closedir(dir);
   close(newdir);
   return 0;
+#else
+  return -1;
+#endif
 }
 
 int handle_deref(const char* p, const struct stat* st, const lsContext& ctx){
@@ -129,6 +133,7 @@ int handle_deref(const char* p, const struct stat* st, const lsContext& ctx){
 // true iff the path was directly listed on the command line.
 int handle_path(int dirfd, const std::string& pdir, const char* p, const lsContext& ctx, bool toplevel){
   struct stat st;
+#ifndef __MINGW64__
   if(fstatat(dirfd, p, &st, AT_NO_AUTOMOUNT)){
     std::cerr << "Error running fstatat(" << p << "): " << strerror(errno) << std::endl;
     return -1;
@@ -141,6 +146,9 @@ int handle_path(int dirfd, const std::string& pdir, const char* p, const lsConte
     }
   }
   return handle_inode(pdir, p, &st, ctx);
+#else
+  return -1;
+#endif
 }
 
 // return long-term return code
diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
index 629ea06d6a..a23638f564 100644
--- a/src/media/ffmpeg.c
+++ b/src/media/ffmpeg.c
@@ -673,11 +673,11 @@ int ffmpeg_init(int logl){
   return 0;
 }
 
-void ffmpeg_printbanner(const notcurses* nc){
-  fprintf(nc->ttyfp, "avformat %u.%u.%u avutil %u.%u.%u swscale %u.%u.%u\n",
-          LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO,
-          LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO,
-          LIBSWSCALE_VERSION_MAJOR, LIBSWSCALE_VERSION_MINOR, LIBSWSCALE_VERSION_MICRO);
+void ffmpeg_printbanner(fbuf* f){
+  fbuf_printf(f, "avformat %u.%u.%u avutil %u.%u.%u swscale %u.%u.%u\n",
+              LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO,
+              LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO,
+              LIBSWSCALE_VERSION_MAJOR, LIBSWSCALE_VERSION_MINOR, LIBSWSCALE_VERSION_MICRO);
 }
 
 void ffmpeg_details_destroy(ncvisual_details* deets){
diff --git a/src/media/none.c b/src/media/none.c
index 3894e8df8b..beecfd2f46 100644
--- a/src/media/none.c
+++ b/src/media/none.c
@@ -4,8 +4,8 @@
 #include "internal.h"
 
 static void
-printbanner(const notcurses* nc){
-  fprintf(nc->ttyfp, "built without multimedia support\n");
+printbanner(fbuf* f){
+  fbuf_puts(f, "built without multimedia support\n");
 }
 
 const ncvisual_implementation local_visual_implementation = {
diff --git a/src/media/oiio.cpp b/src/media/oiio.cpp
index 5fb9dad16b..1662d0efeb 100644
--- a/src/media/oiio.cpp
+++ b/src/media/oiio.cpp
@@ -195,8 +195,10 @@ auto oiio_destroy(ncvisual* ncv) -> void {
 }
 
 // FIXME would be nice to have OIIO::attributes("libraries") in here
-void oiio_printbanner(const struct notcurses* nc){
-  fprintf(nc->ttyfp, "openimageio %s\n", OIIO_VERSION_STRING);
+void oiio_printbanner(fbuf* f){
+  fbuf_puts(f, "openimageio ");
+  fbuf_puts(f, OIIO_VERSION_STRING);
+  fbuf_putc(f, '\n');
 }
 
 #endif
diff --git a/src/media/oiio.h b/src/media/oiio.h
index 2f5126b41e..0d5d13e039 100644
--- a/src/media/oiio.h
+++ b/src/media/oiio.h
@@ -9,7 +9,7 @@ extern "C" {
 
 int oiio_decode(ncvisual* nc);
 struct ncvisual_details* oiio_details_init(void);
-void oiio_printbanner(const struct notcurses* nc);
+void oiio_printbanner(fbuf* f);
 void oiio_details_seed(struct ncvisual* ncv);
 int oiio_blit(ncvisual* ncv, int rows, int cols,
               struct ncplane* n, const struct blitset* bset,
diff --git a/src/poc/dirgb.c b/src/poc/dirgb.c
index 6997ba1395..d47ec48e27 100644
--- a/src/poc/dirgb.c
+++ b/src/poc/dirgb.c
@@ -34,14 +34,10 @@ print_gb(struct ncdirect* nc, int r, int total){
 static int
 print_rgb8(struct ncdirect* nc, int total){
   if(rand() % 2){
-    if(ncdirect_off_styles(nc, NCSTYLE_ITALIC)){
-      return -1;
-    }
+    ncdirect_off_styles(nc, NCSTYLE_ITALIC);
   }
   if(rand() % 16 == 0){
-    if(ncdirect_on_styles(nc, NCSTYLE_ITALIC)){
-      return -1;
-    }
+    ncdirect_on_styles(nc, NCSTYLE_ITALIC);
   }
   for(int r = 0 ; r <= total && r < 256 ; r += 4){
     if(print_gb(nc, r, total)){
@@ -93,9 +89,7 @@ int main(void){
     goto err;
   }
 
-  if(ncdirect_set_styles(nc, NCSTYLE_ITALIC)){
-    goto err;
-  }
+  ncdirect_set_styles(nc, NCSTYLE_ITALIC);
   for(int t = 768 ; t ; t -= 4){
     if(print_rgb8(nc, t)){
       goto err;


Test patch:
diff --git a/.github/workflows/windows_test.yml b/.github/workflows/windows_test.yml
new file mode 100644
index 0000000000..b1f9623c5d
--- /dev/null
+++ b/.github/workflows/windows_test.yml
@@ -0,0 +1,94 @@
+---
+name: Windows
+
+on:
+  pull_request:
+  push:
+    branches:
+      - master
+
+jobs:
+  tests:
+    defaults:
+      run:
+        shell: msys2 {0}
+    env:
+      COLORTERM: truecolor
+      NPROC: 2
+      TERM: xterm
+    name: 🏁 build, test, & install
+    runs-on: windows-latest
+
+    steps:
+
+      - name: Install tools and libraries via MSYS2
+        uses: msys2/setup-msys2@v2
+        with:
+          msystem: UCRT64
+          update: true
+          install: >
+            base-devel
+            git
+            mingw-w64-ucrt-x86_64-cmake
+            mingw-w64-ucrt-x86_64-doctest
+            mingw-w64-ucrt-x86_64-ffmpeg
+            mingw-w64-ucrt-x86_64-libunistring
+            mingw-w64-ucrt-x86_64-ncurses
+            mingw-w64-ucrt-x86_64-rust
+            mingw-w64-ucrt-x86_64-toolchain
+
+      - uses: actions/checkout@v2
+
+      - name: Build libqrcodegen
+        run: |
+          mkdir "${HOME}/repos" && cd "${HOME}/repos"
+          git clone https://github.com/nayuki/QR-Code-generator.git
+          cd QR-Code-generator/c
+          make
+          cd /ucrt64/include && \
+            ln -s "${HOME}/repos/QR-Code-generator/c" ./qrcodegen
+          cd /ucrt64/lib && \
+            ln -s "${HOME}/repos/QR-Code-generator/c/libqrcodegen.a" ./libqrcodegen.a
+
+      - name: cmake
+        run: |
+          mkdir build && cd build
+          cmake .. \
+            -G "MSYS Makefiles" \
+            -DCMAKE_BUILD_TYPE=Release \
+            -DUSE_DOCTEST=off \
+            -DUSE_PANDOC=off \
+            -DUSE_READLINE=off \
+            -DUSE_QRCODEGEN=on
+
+      - name: make
+        run: |
+          cd build
+          make -j${NPROC}
+
+      - name: ctest
+        run: |
+          cd build
+          ctest --output-on-failure
+
+      - name: make install
+        run: |
+          cd build
+          make install
+
+      - name: python wrappers
+        run: |
+          python3 -m pip install --upgrade pip
+          pip install pypandoc
+          cd cffi
+          python3 setup.py sdist build
+          python3 setup.py install
+          notcurses-pydemo > /dev/null
+          ncdirect-pydemo > /dev/null
+
+      - name: rust wrappers
+        run: |
+          cd rust
+          rustc --version
+          cargo build
+          cargo t_all
diff --git a/src/tests/sixel.cpp b/src/tests/sixel.cpp
index 120ec98c26..625bff3387 100644
--- a/src/tests/sixel.cpp
+++ b/src/tests/sixel.cpp
@@ -5,7 +5,7 @@
 
 // convert the sprixel at s having pixel dimensions dimyXdimx to an rgb(a)
 // matrix for easier analysis. breaks on malformed sixels.
-std::vector<uint32_t> sixel_to_rgb(const char* s, int dimy, int dimx) {
+std::vector<uint32_t> sixel_to_rgb(const char* s, size_t len, int dimy, int dimx) {
   std::vector<uint32_t> bmap(dimy * dimx, 0x00000000ull);
   std::vector<uint32_t> colors;
   // first we skip the header
@@ -25,7 +25,8 @@ std::vector<uint32_t> sixel_to_rgb(const char* s, int dimy, int dimx) {
   unsigned x = 0;
   unsigned y = 0;
   unsigned rle = 1;
-  while(*s){
+  const char* begin = s;
+  while((size_t)(s - begin) < len){
     if(*s == '\e'){
       break;
     }
@@ -166,7 +167,8 @@ TEST_CASE("Sixels") {
     auto newn = ncvisual_render(nc_, ncv, &vopts);
     CHECK(newn);
     CHECK(0 == notcurses_render(nc_));
-    auto rgb = sixel_to_rgb(newn->sprite->glyph, newn->sprite->pixy, newn->sprite->pixx);
+    auto rgb = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
+                            newn->sprite->pixy, newn->sprite->pixx);
     for(int y = 0 ; y < newn->sprite->pixy ; ++y){
       for(int x = 0 ; x < newn->sprite->pixx ; ++x){
 //fprintf(stderr, "%03d/%03d NCV: %08x RGB: %08x\n", y, x, ncv->data[y * newn->sprite->pixx + x], rgb[y * newn->sprite->pixx + x]);
@@ -186,7 +188,8 @@ TEST_CASE("Sixels") {
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     auto newn = ncvisual_render(nc_, ncv, &vopts);
     CHECK(newn);
-    auto rgbold = sixel_to_rgb(newn->sprite->glyph, newn->sprite->pixy, newn->sprite->pixx);
+    auto rgbold = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
+                               newn->sprite->pixy, newn->sprite->pixx);
 //print_bmap(rgbold, newn->sprite->pixy, newn->sprite->pixx);
     CHECK(0 == notcurses_render(nc_));
     struct ncplane_options nopts = {
@@ -208,7 +211,8 @@ TEST_CASE("Sixels") {
     CHECK(0 == notcurses_render(nc_));
     // FIXME at this point currently, we get a degraded back of the orca
     // test via conversion back to image? unsure
-    auto rgbnew = sixel_to_rgb(newn->sprite->glyph, newn->sprite->pixy, newn->sprite->pixx);
+    auto rgbnew = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
+                               newn->sprite->pixy, newn->sprite->pixx);
 //print_bmap(rgbnew, newn->sprite->pixy, newn->sprite->pixx);
     CHECK(0 == ncplane_destroy(newn));
     CHECK(0 == ncplane_destroy(blockerplane));