1 #include "linux.h"
2 #include "version.h"
3 #include "egcpool.h"
4 #include "internal.h"
5 #include <time.h>
6 #include <fcntl.h>
7 #include <errno.h>
8 #include <stdio.h>
9 #include <limits.h>
10 #include <string.h>
11 #include <unistd.h>
12 #include <stdlib.h>
13 #include <unistr.h>
14 #include <locale.h>
15 #include <uniwbrk.h>
16 #include <inttypes.h>
17 #include <notcurses/direct.h>
18 #include "compat/compat.h"
19 #include "unixsig.h"
20 #include "banner.h"
21 
22 #define ESC "\x1b"
23 
notcurses_version_components(int * major,int * minor,int * patch,int * tweak)24 void notcurses_version_components(int* major, int* minor, int* patch, int* tweak){
25   *major = NOTCURSES_VERNUM_MAJOR;
26   *minor = NOTCURSES_VERNUM_MINOR;
27   *patch = NOTCURSES_VERNUM_PATCH;
28   *tweak = atoi(NOTCURSES_VERSION_TWEAK);
29 }
30 
notcurses_enter_alternate_screen(notcurses * nc)31 int notcurses_enter_alternate_screen(notcurses* nc){
32   if(nc->tcache.ttyfd < 0){
33     return -1;
34   }
35   if(enter_alternate_screen(nc->tcache.ttyfd, nc->ttyfp, &nc->tcache, nc->flags & NCOPTION_DRAIN_INPUT)){
36     return -1;
37   }
38   ncplane_set_scrolling(notcurses_stdplane(nc), false);
39   return 0;
40 }
41 
notcurses_leave_alternate_screen(notcurses * nc)42 int notcurses_leave_alternate_screen(notcurses* nc){
43   if(nc->tcache.ttyfd < 0){
44     return -1;
45   }
46   if(leave_alternate_screen(nc->tcache.ttyfd, nc->ttyfp,
47                             &nc->tcache, nc->flags & NCOPTION_DRAIN_INPUT)){
48     return -1;
49   }
50   // move to the end of our output
51   if(nc->rstate.logendy < 0){
52     return 0;
53   }
54   ncplane_cursor_move_yx(notcurses_stdplane(nc), nc->rstate.logendy, nc->rstate.logendx);
55   return 0;
56 }
57 
58 // reset the current colors, styles, and palette. called on startup (to purge
59 // any preexisting styling) and shutdown (to not affect further programs).
reset_term_attributes(const tinfo * ti,fbuf * f)60 int reset_term_attributes(const tinfo* ti, fbuf* f){
61   int ret = 0;
62   const char* esc;
63   if((esc = get_escape(ti, ESCAPE_OP)) && fbuf_emit(f, esc)){
64     ret = -1;
65   }
66   if((esc = get_escape(ti, ESCAPE_SGR0)) && fbuf_emit(f, esc)){
67     ret = -1;
68   }
69   return ret;
70 }
71 
72 // attempt to restore the palette. if XT{PUSH,POP}COLORS is supported, use
73 // XTPOPCOLORS. if we can program individual colors, and we read the palette,
74 // reload it from our initial capture. otherwise, use "oc" if available; this
75 // will blow away any preexisting palette in favor of the default. if we've
76 // never touched the palette, don't bother trying to restore it (unless we're
77 // using XTPOPCOLORS, since in that case we always used XTPUSHCOLORS).
reset_term_palette(const tinfo * ti,fbuf * f,unsigned touchedpalette)78 int reset_term_palette(const tinfo* ti, fbuf* f, unsigned touchedpalette){
79   int ret = 0;
80   const char* esc;
81   if((esc = get_escape(ti, ESCAPE_RESTORECOLORS))){
82     loginfo("restoring palette via xtpopcolors\n");
83     if(fbuf_emit(f, esc)){
84       ret = -1;
85     }
86     return ret;
87   }
88   if(!touchedpalette){
89     return 0;
90   }
91   if(ti->caps.can_change_colors && ti->maxpaletteread > -1){
92     loginfo("restoring saved palette (%d)\n", ti->maxpaletteread + 1);
93     esc = get_escape(ti, ESCAPE_INITC);
94     for(int z = 0 ; z < ti->maxpaletteread ; ++z){
95       unsigned r, g, b;
96       ncchannel_rgb8(ti->originalpalette.chans[z], &r, &g, &b);
97       // Need convert RGB values [0..256) to [0..1000], ugh
98       r = r * 1000 / 255;
99       g = g * 1000 / 255;
100       b = b * 1000 / 255;
101       if(fbuf_emit(f, tiparm(esc, z, r, g, b)) < 0){
102         return -1;
103       }
104     }
105   }else if((esc = get_escape(ti, ESCAPE_OC))){
106     loginfo("resetting palette\n");
107     if(fbuf_emit(f, esc)){
108       ret = -1;
109     }
110   }else{
111     logwarn("no method known to restore palette\n");
112   }
113   return ret;
114 }
115 
116 // Do the minimum necessary stuff to restore the terminal, then return. This is
117 // the end of the line for fatal signal handlers. notcurses_stop() will go on
118 // to tear down and account for internal structures. note that we do lots of
119 // shit here that is unsafe within a signal handler =[ FIXME.
120 static int
notcurses_stop_minimal(void * vnc)121 notcurses_stop_minimal(void* vnc){
122   notcurses* nc = vnc;
123   int ret = 0;
124   ret |= drop_signals(nc);
125   // collect output into the memstream buffer, and then dump it directly using
126   // blocking_write(), to avoid problems with unreliable fflush().
127   fbuf* f = &nc->rstate.f;
128   fbuf_reset(f);
129   // be sure to write the restoration sequences *prior* to running rmcup, as
130   // they apply to the screen (alternate or otherwise) we're actually using.
131   const char* esc;
132   ret |= reset_term_palette(&nc->tcache, f, nc->touched_palette);
133   ret |= reset_term_attributes(&nc->tcache, f);
134   if((esc = get_escape(&nc->tcache, ESCAPE_RMKX)) && fbuf_emit(f, esc)){
135     ret = -1;
136   }
137   const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM);
138   if(cnorm && fbuf_emit(f, cnorm)){
139     ret = -1;
140   }
141   if(fbuf_flush(f, nc->ttyfp)){
142     ret = -1;
143   }
144   if(nc->tcache.ttyfd >= 0){
145     ret |= notcurses_mice_disable(nc);
146     if(nc->tcache.tpreserved){
147       ret |= tcsetattr(nc->tcache.ttyfd, TCSAFLUSH, nc->tcache.tpreserved);
148     }
149     // don't use use leave_alternate_screen() here; we need pop the keyboard
150     // whether we're in regular or alternate screen, and we need it done
151     // before returning to the regular screen if we're in the alternate. if
152     // we drained input, we never sent a keyboard modifier; send none now.
153     if(!(nc->flags & NCOPTION_DRAIN_INPUT)){
154       if(nc->tcache.kbdlevel){
155         if(tty_emit(KKEYBOARD_POP, nc->tcache.ttyfd)){
156           ret = -1;
157         }
158       }else{
159         if(tty_emit(XTMODKEYSUNDO, nc->tcache.ttyfd)){
160           ret = -1;
161         }
162       }
163     }
164     if(nc->tcache.in_alt_screen){
165       if((esc = get_escape(&nc->tcache, ESCAPE_RMCUP))){
166         if(tty_emit(esc, nc->tcache.ttyfd)){
167           ret = -1;
168         }
169         nc->tcache.in_alt_screen = 0;
170       }
171     }
172   }
173   logdebug("restored terminal, returning %d\n", ret);
174   return ret;
175 }
176 
177 static const char NOTCURSES_VERSION[] =
178  NOTCURSES_VERSION_MAJOR "."
179  NOTCURSES_VERSION_MINOR "."
180  NOTCURSES_VERSION_PATCH;
181 
notcurses_version(void)182 const char* notcurses_version(void){
183   return NOTCURSES_VERSION;
184 }
185 
ncplane_set_userptr(ncplane * n,void * opaque)186 void* ncplane_set_userptr(ncplane* n, void* opaque){
187   void* ret = n->userptr;
188   n->userptr = opaque;
189   return ret;
190 }
191 
ncplane_userptr(ncplane * n)192 void* ncplane_userptr(ncplane* n){
193   return n->userptr;
194 }
195 
ncplane_userptr_const(const ncplane * n)196 const void* ncplane_userptr_const(const ncplane* n){
197   return n->userptr;
198 }
199 
200 // is the cursor in an invalid position? it never should be, but it's probably
201 // better to make sure (it's cheap) than to read from/write to random crap.
202 static bool
cursor_invalid_p(const ncplane * n)203 cursor_invalid_p(const ncplane* n){
204   if(n->y >= n->leny || n->x >= n->lenx){
205     return true;
206   }
207   return false;
208 }
209 
ncplane_at_cursor(ncplane * n,uint16_t * stylemask,uint64_t * channels)210 char* ncplane_at_cursor(ncplane* n, uint16_t* stylemask, uint64_t* channels){
211   return ncplane_at_yx(n, n->y, n->x, stylemask, channels);
212 }
213 
ncplane_at_yx(const ncplane * n,int y,int x,uint16_t * stylemask,uint64_t * channels)214 char* ncplane_at_yx(const ncplane* n, int y, int x, uint16_t* stylemask, uint64_t* channels){
215   if(y < 0){
216     if(y != -1){
217       logerror("invalid y: %d\n", y);
218       return NULL;
219     }
220     y = n->y;
221   }
222   if(x < 0){
223     if(x != -1){
224       logerror("invalid x: %d\n", x);
225       return NULL;
226     }
227     x = n->x;
228   }
229   if((unsigned)y >= n->leny || (unsigned)x >= n->lenx){
230     logerror("invalid coordinates: %d/%d\n", y, x);
231     return NULL;
232   }
233   const nccell* yx = &n->fb[nfbcellidx(n, y, x)];
234   // if we're the right side of a wide glyph, we return the main glyph
235   if(nccell_wide_right_p(yx)){
236     return ncplane_at_yx(n, y, x - 1, stylemask, channels);
237   }
238   char* ret = nccell_extract(n, yx, stylemask, channels);
239   if(ret == NULL){
240     return NULL;
241   }
242 //fprintf(stderr, "GOT [%s]\n", ret);
243   if(strcmp(ret, "") == 0){
244     free(ret);
245     ret = nccell_strdup(n, &n->basecell);
246     if(ret == NULL){
247       return NULL;
248     }
249     if(stylemask){
250       *stylemask = n->basecell.stylemask;
251     }
252   }
253   // FIXME load basecell channels if appropriate
254   return ret;
255 }
256 
ncplane_at_cursor_cell(ncplane * n,nccell * c)257 int ncplane_at_cursor_cell(ncplane* n, nccell* c){
258   return ncplane_at_yx_cell(n, n->y, n->x, c);
259 }
260 
ncplane_at_yx_cell(ncplane * n,int y,int x,nccell * c)261 int ncplane_at_yx_cell(ncplane* n, int y, int x, nccell* c){
262   if(y < 0){
263     if(y != -1){
264       logerror("invalid y: %d\n", y);
265       return -1;
266     }
267     y = n->y;
268   }
269   if(x < 0){
270     if(x != -1){
271       logerror("invalid x: %d\n", x);
272       return -1;
273     }
274     x = n->x;
275   }
276   if((unsigned)y >= n->leny || (unsigned)x >= n->lenx){
277     logerror("invalid coordinates: %d/%d\n", y, x);
278     return -1;
279   }
280   nccell* targ = ncplane_cell_ref_yx(n, y, x);
281   if(nccell_duplicate(n, c, targ)){
282     return -1;
283   }
284   // FIXME take base cell into account where necessary!
285   return strlen(nccell_extended_gcluster(n, targ));
286 }
287 
ncplane_dim_yx(const ncplane * n,unsigned * rows,unsigned * cols)288 void ncplane_dim_yx(const ncplane* n, unsigned* rows, unsigned* cols){
289   if(rows){
290     *rows = n->leny;
291   }
292   if(cols){
293     *cols = n->lenx;
294   }
295 }
296 
297 // anyone calling this needs ensure the ncplane's framebuffer is updated
298 // to reflect changes in geometry. also called at startup for standard plane.
299 // sets |cgeo_changed| high iff the cell geometry changed (will happen on a
300 //  resize, and on a font resize if the pixel geometry does not change).
301 // sets |pgeo_changed| high iff the cell-pixel geometry changed (will happen
302 //  on a font resize).
update_term_dimensions(unsigned * rows,unsigned * cols,tinfo * tcache,int margin_b,unsigned * cgeo_changed,unsigned * pgeo_changed)303 int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache,
304                            int margin_b, unsigned* cgeo_changed, unsigned* pgeo_changed){
305   *pgeo_changed = 0;
306   *cgeo_changed = 0;
307   // if we're not a real tty, we presumably haven't changed geometry, return
308   if(tcache->ttyfd < 0){
309     if(rows){
310       *rows = tcache->default_rows;
311     }
312     if(cols){
313       *cols = tcache->default_cols;
314     }
315     tcache->cellpxy = 0;
316     tcache->cellpxx = 0;
317     return 0;
318   }
319   unsigned rowsafe, colsafe;
320   if(rows == NULL){
321     rows = &rowsafe;
322     rowsafe = tcache->dimy;
323   }
324   if(cols == NULL){
325     cols = &colsafe;
326     colsafe = tcache->dimx;
327   }
328 #ifndef __MINGW64__
329   struct winsize ws;
330   if(tiocgwinsz(tcache->ttyfd, &ws)){
331     return -1;
332   }
333   *rows = ws.ws_row;
334   *cols = ws.ws_col;
335   unsigned cpixy;
336   unsigned cpixx;
337 #ifdef __linux__
338   if(tcache->linux_fb_fd >= 0){
339     get_linux_fb_pixelgeom(tcache, &tcache->pixy, &tcache->pixx);
340     cpixy = tcache->pixy / *rows;
341     cpixx = tcache->pixx / *cols;
342   }else{
343 #else
344   {
345 #endif
346     // we might have the pixel geometry from CSI14t, so don't override a valid
347     // earlier response with 0s from the ioctl. we do want to fire off a fresh
348     // CSI14t in this case, though FIXME.
349     if(ws.ws_ypixel){
350       tcache->pixy = ws.ws_ypixel;
351       tcache->pixx = ws.ws_xpixel;
352     }
353     // update even if we didn't get values just now, because we need set
354     // cellpx{y,x} up from an initial CSI14n, which set only pix{y,x}.
355     cpixy = ws.ws_row ? tcache->pixy / ws.ws_row : 0;
356     cpixx = ws.ws_col ? tcache->pixx / ws.ws_col : 0;
357   }
358   if(tcache->cellpxy != cpixy){
359     tcache->cellpxy = cpixy;
360     *pgeo_changed = 1;
361   }
362   if(tcache->cellpxx != cpixx){
363     tcache->cellpxx = cpixx;
364     *pgeo_changed = 1;
365   }
366   if(tcache->cellpxy == 0 || tcache->cellpxx == 0){
367     tcache->pixel_draw = NULL; // disable support
368   }
369 #else
370   CONSOLE_SCREEN_BUFFER_INFO csbi;
371   // There is the buffer itself, which is similar in function to the scrollback
372   // buffer in a Linux terminal, and there is the display window, which is the
373   // visible view of that buffer. The addressable area (from a VT point of
374   // view) spans the width of the buffer, but the height of the display window.
375   //
376   //  +--------------------------+      ^
377   //  |                          |      |
378   //  |                          |      |
379   //  +-----+--------------+-----+ ^ w  | b
380   //  |XXXXX|XXXXXXXXXXXXXX|XXXXX| | i  | u
381   //  |XXXXX|XXXXXXXXXXXXXX|XXXXX| | n  | f
382   //  |XXXXX|XXXXXXXXXXXXXX|XXXXX| | d  | f
383   //  |XXXXX|XXXXXXXXXXXXXX|XXXXX| | o  | e
384   //  +-----+--------------+-----+ v w  | r
385   //  |                          |      |
386   //  |                          |      |
387   //  +--------------------------+      v
388   //
389   //      <--- window --->
390   //
391   //<--------- buffer --------->
392   //
393   // Because the buffer extends past the bottom of the display window, a user
394   // can potentially scroll down beyond what would normally be thought of as the
395   // end of the buffer. Because the buffer can be wider than the display
396   // window, the user can scroll horizontally to view parts of the addressable
397   // area that aren't currently visible.
398   if(GetConsoleScreenBufferInfo(tcache->outhandle, &csbi)){
399     *cols = csbi.dwSize.X;
400     *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
401   }else{
402     *rows = tcache->default_rows;
403     *cols = tcache->default_cols;
404   }
405 #endif
406   if(tcache->dimy != *rows){
407     tcache->dimy = *rows;
408     *cgeo_changed = 1;
409   }
410   if(tcache->dimx != *cols){
411     tcache->dimx = *cols;
412     *cgeo_changed = 1;
413   }
414   if(tcache->sixel_maxy_pristine){
415     int sixelrows = *rows - 1;
416     // if the bottom margin is at least one row, we can draw into the last
417     // row of our visible area. we must leave the true bottom row alone.
418     if(margin_b){
419       ++sixelrows;
420     }
421     tcache->sixel_maxy = sixelrows * tcache->cellpxy;
422     if(tcache->sixel_maxy > tcache->sixel_maxy_pristine){
423       tcache->sixel_maxy = tcache->sixel_maxy_pristine;
424     }
425   }
426   return 0;
427 }
428 
429 // destroy the sprixels of an ncpile (this will not hide the sprixels)
430 static void
431 free_sprixels(ncpile* n){
432   while(n->sprixelcache){
433     sprixel* tmp = n->sprixelcache->next;
434     sprixel_free(n->sprixelcache);
435     n->sprixelcache = tmp;
436   }
437 }
438 
439 // destroy an empty ncpile. only call with pilelock held.
440 static void
441 ncpile_destroy(ncpile* pile){
442   if(pile){
443     pile->prev->next = pile->next;
444     pile->next->prev = pile->prev;
445     free_sprixels(pile);
446     free(pile->crender);
447     free(pile);
448   }
449 }
450 
451 void free_plane(ncplane* p){
452   if(p){
453     // ncdirect fakes an ncplane with no ->pile
454     if(ncplane_pile(p)){
455       notcurses* nc = ncplane_notcurses(p);
456       pthread_mutex_lock(&nc->stats.lock);
457         --ncplane_notcurses(p)->stats.s.planes;
458         ncplane_notcurses(p)->stats.s.fbbytes -= sizeof(*p->fb) * p->leny * p->lenx;
459       pthread_mutex_unlock(&nc->stats.lock);
460       if(p->above == NULL && p->below == NULL){
461         pthread_mutex_lock(&nc->pilelock);
462           ncpile_destroy(ncplane_pile(p));
463         pthread_mutex_unlock(&nc->pilelock);
464       }
465     }
466     if(p->widget){
467       void* w = p->widget;
468       void (*wdestruct)(void*) = p->wdestruct;
469       p->widget = NULL;
470       p->wdestruct = NULL;
471       logdebug("calling widget destructor %p(%p)\n", wdestruct, w);
472       wdestruct(w);
473       logdebug("got the widget\n");
474     }
475     if(p->sprite){
476       sprixel_hide(p->sprite);
477     }
478     destroy_tam(p);
479     egcpool_dump(&p->pool);
480     free(p->name);
481     free(p->fb);
482     free(p);
483   }
484 }
485 
486 // create a new ncpile. only call with pilelock held. the return value
487 // was assigned to n->pile.
488 __attribute__((malloc))
489 static ncpile*
490 make_ncpile(notcurses* nc, ncplane* n){
491   ncpile* ret = malloc(sizeof(*ret));
492   if(ret){
493     ret->nc = nc;
494     ret->top = n;
495     ret->bottom = n;
496     ret->roots = n;
497     n->bprev = &ret->roots;
498     if(nc->stdplane){ // stdplane (and thus stdpile) has already been created
499       ret->prev = ncplane_pile(nc->stdplane)->prev;
500       ncplane_pile(nc->stdplane)->prev->next = ret;
501       ret->next = ncplane_pile(nc->stdplane);
502       ncplane_pile(nc->stdplane)->prev = ret;
503     }else{
504       ret->prev = ret;
505       ret->next = ret;
506     }
507     n->above = NULL;
508     n->below = NULL;
509     ret->dimy = nc->tcache.dimy;
510     ret->dimx = nc->tcache.dimx;
511     ret->cellpxy = nc->tcache.cellpxy;
512     ret->cellpxx = nc->tcache.cellpxx;
513     ret->crender = NULL;
514     ret->crenderlen = 0;
515     ret->sprixelcache = NULL;
516     ret->scrolls = 0;
517   }
518   n->pile = ret;
519   return ret;
520 }
521 
522 // create a new ncplane at the specified location (relative to the true screen,
523 // having origin at 0,0), having the specified size, and put it at the top of
524 // the planestack. its cursor starts at its origin; its style starts as null.
525 // a plane may exceed the boundaries of the screen, but must have positive
526 // size in both dimensions. bind the plane to 'n', which may be NULL to create
527 // a new pile. if bound to a plane instead, this plane moves when that plane
528 // moves, and coordinates to move to are relative to that plane.
529 // there are two denormalized case we also must handle, that of the "fake"
530 // isolated ncplane created by ncdirect for rendering visuals. in that case
531 // (and only in that case), nc is NULL (as is n). there's also creation of the
532 // initial standard plane, in which case nc is not NULL, but nc->stdplane *is*
533 // (as once more is n).
534 ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
535                               const ncplane_options* nopts){
536   if(nopts->flags >= (NCPLANE_OPTION_FIXED << 1u)){
537     logwarn("provided unsupported flags %016" PRIx64 "\n", nopts->flags);
538   }
539   if(nopts->flags & NCPLANE_OPTION_HORALIGNED || nopts->flags & NCPLANE_OPTION_VERALIGNED){
540     if(n == NULL){
541       logerror("alignment requires a parent plane\n");
542       return NULL;
543     }
544   }
545   if(nopts->flags & NCPLANE_OPTION_MARGINALIZED){
546     if(nopts->rows != 0 || nopts->cols != 0){
547       logerror("geometry specified with margins (r=%d, c=%d)\n",
548                nopts->rows, nopts->cols);
549       return NULL;
550     }
551   }else if(nopts->rows <= 0 || nopts->cols <= 0){
552     logerror("won't create denormalized plane (r=%d, c=%d)\n",
553              nopts->rows, nopts->cols);
554     return NULL;
555   }
556   ncplane* p = malloc(sizeof(*p));
557   if(p == NULL){
558     return NULL;
559   }
560   p->scrolling = nopts->flags & NCPLANE_OPTION_VSCROLL;
561   p->fixedbound = nopts->flags & NCPLANE_OPTION_FIXED;
562   p->autogrow = nopts->flags & NCPLANE_OPTION_AUTOGROW;
563   p->widget = NULL;
564   p->wdestruct = NULL;
565   if(nopts->flags & NCPLANE_OPTION_MARGINALIZED){
566     p->margin_b = nopts->margin_b;
567     p->margin_r = nopts->margin_r;
568     if(n){ // use parent size
569       p->leny = ncplane_dim_y(n);
570       p->lenx = ncplane_dim_x(n);
571     }else{ // use pile size
572       notcurses_term_dim_yx(nc, &p->leny, &p->lenx);
573     }
574     if((p->leny -= p->margin_b) <= 0){
575       p->leny = 1;
576     }
577     if((p->lenx -= p->margin_r) <= 0){
578       p->lenx = 1;
579     }
580   }else{
581     p->leny = nopts->rows;
582     p->lenx = nopts->cols;
583   }
584   size_t fbsize = sizeof(*p->fb) * (p->leny * p->lenx);
585   if((p->fb = malloc(fbsize)) == NULL){
586     logerror("error allocating cellmatrix (r=%d, c=%d)\n",
587              p->leny, p->lenx);
588     free(p);
589     return NULL;
590   }
591   memset(p->fb, 0, fbsize);
592   p->x = p->y = 0;
593   p->logrow = 0;
594   p->sprite = NULL;
595   p->blist = NULL;
596   p->name = strdup(nopts->name ? nopts->name : "");
597   p->halign = NCALIGN_UNALIGNED;
598   p->valign = NCALIGN_UNALIGNED;
599   p->tam = NULL;
600   if(!n){ // new root/standard plane
601     p->absy = nopts->y;
602     p->absx = nopts->x;
603     p->bnext = NULL;
604     p->bprev = NULL;
605     p->boundto = p;
606   }else{ // bound to preexisting pile
607     if(nopts->flags & NCPLANE_OPTION_HORALIGNED){
608       p->absx = ncplane_halign(n, nopts->x, nopts->cols);
609       p->halign = nopts->x;
610     }else{
611       p->absx = nopts->x;
612     }
613     p->absx += n->absx;
614     if(nopts->flags & NCPLANE_OPTION_VERALIGNED){
615       p->absy = ncplane_valign(n, nopts->y, nopts->rows);
616       p->valign = nopts->y;
617     }else{
618       p->absy = nopts->y;
619     }
620     p->absy += n->absy;
621     if( (p->bnext = n->blist) ){
622       n->blist->bprev = &p->bnext;
623     }
624     p->bprev = &n->blist;
625     *p->bprev = p;
626     p->boundto = n;
627   }
628   // FIXME handle top/left margins
629   p->resizecb = nopts->resizecb;
630   p->stylemask = 0;
631   p->channels = 0;
632   egcpool_init(&p->pool);
633   nccell_init(&p->basecell);
634   p->userptr = nopts->userptr;
635   if(nc == NULL){ // fake ncplane backing ncdirect object
636     p->above = NULL;
637     p->below = NULL;
638     p->pile = NULL;
639   }else{
640     pthread_mutex_lock(&nc->pilelock);
641       ncpile* pile = n ? ncplane_pile(n) : NULL;
642       if( (p->pile = pile) ){ // existing pile
643         p->above = NULL;
644         if( (p->below = pile->top) ){ // always happens save initial plane
645           pile->top->above = p;
646         }else{
647           pile->bottom = p;
648         }
649         pile->top = p;
650       }else{ // new pile
651         make_ncpile(nc, p);
652       }
653       pthread_mutex_lock(&nc->stats.lock);
654         nc->stats.s.fbbytes += fbsize;
655         ++nc->stats.s.planes;
656       pthread_mutex_unlock(&nc->stats.lock);
657     pthread_mutex_unlock(&nc->pilelock);
658   }
659   loginfo("created new %dx%d plane \"%s\" @ %dx%d\n",
660           p->leny, p->lenx, p->name ? p->name : "", p->absy, p->absx);
661   return p;
662 }
663 
664 // create an ncplane of the specified dimensions, but do not yet place it in
665 // the z-buffer. clear out all cells. this is for a wholly new context.
666 static ncplane*
667 create_initial_ncplane(notcurses* nc, int dimy, int dimx){
668   ncplane_options nopts = {
669     .y = 0, .x = 0,
670     .rows = dimy - (nc->margin_t + nc->margin_b),
671     .cols = dimx - (nc->margin_l + nc->margin_r),
672     .userptr = NULL,
673     .name = "std",
674     .resizecb = NULL,
675     .flags = 0,
676   };
677   return nc->stdplane = ncplane_new_internal(nc, NULL, &nopts);
678 }
679 
680 ncplane* notcurses_stdplane(notcurses* nc){
681   return nc->stdplane;
682 }
683 
684 const ncplane* notcurses_stdplane_const(const notcurses* nc){
685   return nc->stdplane;
686 }
687 
688 ncplane* ncplane_create(ncplane* n, const ncplane_options* nopts){
689   return ncplane_new_internal(ncplane_notcurses(n), n, nopts);
690 }
691 
692 ncplane* ncpile_create(notcurses* nc, const struct ncplane_options* nopts){
693   return ncplane_new_internal(nc, NULL, nopts);
694 }
695 
696 void ncplane_home(ncplane* n){
697   n->x = 0;
698   n->y = 0;
699 }
700 
701 int ncplane_cursor_move_yx(ncplane* n, int y, int x){
702   if(x < 0){
703     if(x < -1){
704       logerror("negative target x %d\n", x);
705       return -1;
706     }
707   }else if((unsigned)x >= n->lenx){
708     logerror("target x %d >= width %u\n", x, n->lenx);
709     return -1;
710   }else{
711     n->x = x;
712   }
713   if(y < 0){
714     if(y < -1){
715       logerror("negative target y %d\n", y);
716       return -1;
717     }
718   }else if((unsigned)y >= n->leny){
719     logerror("target y %d >= height %u\n", y, n->leny);
720     return -1;
721   }else{
722     n->y = y;
723   }
724   if(cursor_invalid_p(n)){
725     logerror("invalid cursor following move (%d/%d)\n", n->y, n->x);
726     return -1;
727   }
728   return 0;
729 }
730 
731 int ncplane_cursor_move_rel(ncplane* n, int y, int x){
732   if((int)n->y + y == -1){
733     logerror("invalid target y -1\n");
734     return -1;
735   }else if((int)n->x + x == -1){
736     logerror("invalid target x -1\n");
737     return -1;
738   }else return ncplane_cursor_move_yx(n, n->y + y, n->x + x);
739 }
740 
741 ncplane* ncplane_dup(const ncplane* n, void* opaque){
742   int dimy = n->leny;
743   int dimx = n->lenx;
744   const int placey = n->absy;
745   const int placex = n->absx;
746   struct ncplane_options nopts = {
747     .y = placey,
748     .x = placex,
749     .rows = dimy,
750     .cols = dimx,
751     .userptr = opaque,
752     .name = n->name,
753     .resizecb = ncplane_resizecb(n),
754     .flags = 0,
755   };
756   ncplane* newn = ncplane_create(n->boundto, &nopts);
757   if(newn == NULL){
758     return NULL;
759   }
760   // we don't duplicate sprites...though i'm unsure why not
761   size_t fbsize = sizeof(*n->fb) * dimx * dimy;
762   if(egcpool_dup(&newn->pool, &n->pool)){
763     ncplane_destroy(newn);
764     return NULL;
765   }
766   memmove(newn->fb, n->fb, fbsize);
767   // don't use ncplane_cursor_move_yx() here; the cursor could be in an
768   // invalid location, which will be disallowed, failing out.
769   newn->y = n->y;
770   newn->x = n->x;
771   newn->halign = n->halign;
772   newn->stylemask = ncplane_styles(n);
773   newn->channels = ncplane_channels(n);
774   // we dupd the egcpool, so just dup the goffset
775   newn->basecell = n->basecell;
776   return newn;
777 }
778 
779 // call the resize callback for each bound child in turn. we only need to do
780 // the first generation; if they resize, they'll invoke
781 // ncplane_resize_internal(), leading to this function being called anew.
782 int resize_callbacks_children(ncplane* n){
783   int ret = 0;
784   for(struct ncplane* child = n->blist ; child ; child = child->bnext){
785     if(child->resizecb){
786       ret |= child->resizecb(child);
787     }
788   }
789   return ret;
790 }
791 
792 // can be used on stdplane, unlike ncplane_resize() which prohibits it.
793 int ncplane_resize_internal(ncplane* n, int keepy, int keepx,
794                             unsigned keepleny, unsigned keeplenx,
795                             int yoff, int xoff,
796                             unsigned ylen, unsigned xlen){
797   if(keepy < 0 || keepx < 0){ // can't start at negative origin
798     logerror("can't retain negative offset %dx%d\n", keepy, keepx);
799     return -1;
800   }
801   if((!keepleny && keeplenx) || (keepleny && !keeplenx)){ // both must be 0
802     logerror("can't retain null dimension %dx%d\n", keepleny, keeplenx);
803     return -1;
804   }
805   // can't be smaller than keep length
806   if(ylen < keepleny){
807     logerror("can't map in y dimension: %u < %d\n", ylen, keepleny);
808     return -1;
809   }
810   if(xlen < keeplenx){
811     logerror("can't map in x dimension: %u < %d\n", xlen, keeplenx);
812     return -1;
813   }
814   if(ylen <= 0 || xlen <= 0){ // can't resize to trivial or negative size
815     logerror("can't achieve meaningless size %ux%u\n", ylen, xlen);
816     return -1;
817   }
818   unsigned rows, cols;
819   ncplane_dim_yx(n, &rows, &cols);
820   if(keepleny + keepy > rows){
821     logerror("can't keep %d@%d rows from %d\n", keepleny, keepy, rows);
822     return -1;
823   }
824   if(keeplenx + keepx > cols){
825     logerror("can't keep %d@%d cols from %d\n", keeplenx, keepx, cols);
826     return -1;
827   }
828   loginfo("%dx%d @ %d/%d → %u/%u @ %d/%d (want %ux%u@%d/%d)\n", rows, cols, n->absy, n->absx, ylen, xlen, n->absy + keepy + yoff, n->absx + keepx + xoff, keepleny, keeplenx, keepy, keepx);
829   if(n->absy == n->absy + keepy && n->absx == n->absx + keepx &&
830       rows == ylen && cols == xlen){
831     return 0;
832   }
833   notcurses* nc = ncplane_notcurses(n);
834   if(n->sprite){
835     sprixel_hide(n->sprite);
836   }
837   // we're good to resize. we'll need alloc up a new framebuffer, and copy in
838   // those elements we're retaining, zeroing out the rest. alternatively, if
839   // we've shrunk, we will be filling the new structure.
840   int oldarea = rows * cols;
841   int keptarea = keepleny * keeplenx;
842   int newarea = ylen * xlen;
843   size_t fbsize = sizeof(nccell) * newarea;
844   // an important optimization is the case where nothing needs to be moved,
845   // true when either the keptarea is 0 or the old and new x dimensions are
846   // equal, and we're keeping the full x dimension, and any material we're
847   // keeping starts at the top. in this case, we try to realloc() and avoid
848   // any copying whatsoever (we otherwise incur at least one copy due to
849   // always using a new area). set realloced high in this event, so we don't
850   // free anything.
851   nccell* fb;
852   bool realloced = false;
853   if(keptarea == 0 || (cols == xlen && cols == keeplenx && keepy == 0)){
854     if((fb = realloc(n->fb, fbsize)) == NULL){
855       return -1;
856     }
857     realloced = true;
858   }else{
859     if((fb = malloc(fbsize)) == NULL){
860       return -1;
861     }
862   }
863   if(n->tam){
864     loginfo("TAM realloc to %d entries\n", newarea);
865     // FIXME first, free any disposed auxiliary vectors!
866     tament* tmptam = realloc(n->tam, sizeof(*tmptam) * newarea);
867     if(tmptam == NULL){
868       if(!realloced){
869         free(fb);
870       }
871       return -1;
872     }
873     n->tam = tmptam;
874     if(newarea > oldarea){
875       memset(n->tam + oldarea, 0, sizeof(*n->tam) * (newarea - oldarea));
876     }
877   }
878   // update the cursor, if it would otherwise be off-plane
879   if(n->y >= ylen){
880     n->y = ylen - 1;
881   }
882   if(n->x >= xlen){
883     n->x = xlen - 1;
884   }
885   nccell* preserved = n->fb;
886   pthread_mutex_lock(&nc->stats.lock);
887     ncplane_notcurses(n)->stats.s.fbbytes -= sizeof(*preserved) * (rows * cols);
888     ncplane_notcurses(n)->stats.s.fbbytes += fbsize;
889   pthread_mutex_unlock(&nc->stats.lock);
890   n->fb = fb;
891   const int oldabsy = n->absy;
892   // go ahead and move. we can no longer fail at this point. but don't yet
893   // resize, because n->len[xy] are used in fbcellidx() in the loop below. we
894   // don't use ncplane_move_yx(), because we want to planebinding-invariant.
895   n->absy += keepy + yoff;
896   n->absx += keepx + xoff;
897 //fprintf(stderr, "absx: %d keepx: %d xoff: %d\n", n->absx, keepx, xoff);
898   if(keptarea == 0){ // keep nothing, resize/move only.
899     // if we're keeping nothing, dump the old egcspool. otherwise, we go ahead
900     // and keep it. perhaps we ought compact it?
901     memset(fb, 0, sizeof(*fb) * newarea);
902     egcpool_dump(&n->pool);
903   }else if(realloced){
904     // the x dimensions are equal, and we're keeping across the width. only the
905     // y dimension changed. at worst, we need zero some out.
906     unsigned tozorch = (ylen - keepleny) * xlen * sizeof(*fb);
907     if(tozorch){
908       unsigned zorchoff = keepleny * xlen;
909       memset(fb + zorchoff, 0, tozorch);
910     }
911   }else{
912     // we currently have maxy rows of maxx cells each. we will be keeping rows
913     // keepy..keepy + keepleny - 1 and columns keepx..keepx + keeplenx - 1.
914     // anything else is zerod out. itery is the row we're writing *to*, and we
915     // must write to each (and every cell in each).
916     for(unsigned itery = 0 ; itery < ylen ; ++itery){
917       int truey = itery + n->absy;
918       int sourceoffy = truey - oldabsy;
919 //fprintf(stderr, "sourceoffy: %d keepy: %d ylen: %d\n", sourceoffy, keepy, ylen);
920       // if we have nothing copied to this line, zero it out in one go
921       if(sourceoffy < keepy || sourceoffy >= keepy + (int)keepleny){
922 //fprintf(stderr, "writing 0s to line %d of %d\n", itery, ylen);
923         memset(fb + (itery * xlen), 0, sizeof(*fb) * xlen);
924       }else{
925         int copyoff = itery * xlen; // our target at any given time
926         // we do have something to copy, and zero, one, or two regions to zero out
927         unsigned copied = 0;
928         if(xoff < 0){
929           memset(fb + copyoff, 0, sizeof(*fb) * -xoff);
930           copyoff += -xoff;
931           copied += -xoff;
932         }
933         const int sourceidx = nfbcellidx(n, sourceoffy, keepx);
934 //fprintf(stderr, "copying line %d (%d) to %d (%d)\n", sourceoffy, sourceidx, copyoff / xlen, copyoff);
935         memcpy(fb + copyoff, preserved + sourceidx, sizeof(*fb) * keeplenx);
936         copyoff += keeplenx;
937         copied += keeplenx;
938         if(xlen > copied){
939           memset(fb + copyoff, 0, sizeof(*fb) * (xlen - copied));
940         }
941       }
942     }
943   }
944   n->lenx = xlen;
945   n->leny = ylen;
946   if(!realloced){
947     free(preserved);
948   }
949   return resize_callbacks_children(n);
950 }
951 
952 int ncplane_resize(ncplane* n, int keepy, int keepx,
953                    unsigned keepleny, unsigned keeplenx,
954                    int yoff, int xoff,
955                    unsigned ylen, unsigned xlen){
956   if(n == ncplane_notcurses(n)->stdplane){
957 //fprintf(stderr, "Can't resize standard plane\n");
958     return -1;
959   }
960   return ncplane_resize_internal(n, keepy, keepx, keepleny, keeplenx,
961                                  yoff, xoff, ylen, xlen);
962 }
963 
964 int ncplane_destroy(ncplane* ncp){
965   if(ncp == NULL){
966     return 0;
967   }
968   if(ncplane_notcurses(ncp)->stdplane == ncp){
969     logerror("Won't destroy standard plane\n");
970     return -1;
971   }
972 //notcurses_debug(ncplane_notcurses(ncp), stderr);
973   loginfo("Destroying %dx%d plane \"%s\" @ %dx%d\n",
974           ncp->leny, ncp->lenx, ncp->name ? ncp->name : NULL, ncp->absy, ncp->absx);
975   int ret = 0;
976   // dissolve our binding from behind (->bprev is either NULL, or its
977   // predecessor on the bound list's ->bnext, or &ncp->boundto->blist)
978   if(ncp->bprev){
979     if( (*ncp->bprev = ncp->bnext) ){
980       ncp->bnext->bprev = ncp->bprev;
981     }
982   }else if(ncp->bnext){
983     //assert(ncp->boundto->blist == ncp);
984     ncp->bnext->bprev = NULL;
985   }
986   // recursively reparent our children to the plane to which we are bound.
987   // this will extract each one from the sibling list.
988   struct ncplane* bound = ncp->blist;
989   while(bound){
990     struct ncplane* tmp = bound->bnext;
991     ncplane* bindto = ((ncp == ncp->boundto) ? bound : ncp->boundto);
992     if(ncplane_reparent_family(bound, bindto) == NULL){
993       ret = -1;
994     }
995     bound = tmp;
996   }
997   // extract ourselves from the z-axis. do this *after* reparenting, in case
998   // reparenting shifts up the z-axis somehow (though i don't think it can,
999   // at least not within a pile?).
1000   if(ncp->above){
1001     ncp->above->below = ncp->below;
1002   }else{
1003     ncplane_pile(ncp)->top = ncp->below;
1004   }
1005   if(ncp->below){
1006     ncp->below->above = ncp->above;
1007   }else{
1008     ncplane_pile(ncp)->bottom = ncp->above;
1009   }
1010   free_plane(ncp);
1011   return ret;
1012 }
1013 
1014 int ncplane_destroy_family(ncplane *ncp){
1015   if(ncp == NULL){
1016     return 0;
1017   }
1018   if(ncplane_notcurses(ncp)->stdplane == ncp){
1019     logerror("Won't destroy standard plane\n");
1020     return -1;
1021   }
1022   int ret = 0;
1023   while(ncp->blist){
1024     ret |= ncplane_destroy_family(ncp->blist);
1025   }
1026   ret |= ncplane_destroy(ncp);
1027   return ret;
1028 }
1029 
1030 // it's critical that we're using UTF-8 encoding if at all possible. since the
1031 // client might not have called setlocale(2) (if they weren't reading the
1032 // directions...), go ahead and try calling setlocale(LC_ALL, "") and then
1033 // setlocale(LC_CTYPE, "C.UTF-8") ourselves *iff* we're not using UTF-8 *and*
1034 // LANG is not explicitly set to "C" nor "POSIX". this still requires the user
1035 // to have a proper locale generated and available on disk. either way, they're
1036 // going to get a diagnostic (unless the user has explicitly configured a LANG
1037 // of "C" or "POSIX"). recommended practice is for the client code to have
1038 // called setlocale() themselves, and set the NCOPTION_INHIBIT_SETLOCALE flag.
1039 // if that flag is set, we take the locale and encoding as we get them.
1040 void init_lang(void){
1041   char* setret;
1042 #ifdef __MINGW64__
1043   if((setret = setlocale(LC_ALL, ".UTF8")) == NULL){
1044     logwarn("couldn't set LC_ALL to utf8\n");
1045   }
1046 #endif
1047   const char* encoding = nl_langinfo(CODESET);
1048   if(encoding && !strcmp(encoding, "UTF-8")){
1049     return; // already utf-8, great!
1050   }
1051   const char* lang = getenv("LANG");
1052   // if LANG was explicitly set to C/POSIX, life sucks, roll with it
1053   if(lang && (!strcmp(lang, "C") || !strcmp(lang, "POSIX"))){
1054     loginfo("LANG was explicitly set to %s, not changing locale\n", lang);
1055     return;
1056   }
1057 #ifndef __MINGW64__
1058   if((setret = setlocale(LC_ALL, "")) == NULL){
1059     logwarn("setting locale based on LANG failed\n");
1060   }
1061 #endif
1062   encoding = nl_langinfo(CODESET);
1063   if(encoding && !strcmp(encoding, "UTF-8")){
1064     loginfo("Set locale from LANG; client should call setlocale(2)!\n");
1065     return;
1066   }
1067   setlocale(LC_CTYPE, "C.UTF-8");
1068   encoding = nl_langinfo(CODESET);
1069   if(encoding && !strcmp(encoding, "UTF-8")){
1070     loginfo("Forced UTF-8 encoding; client should call setlocale(2)!\n");
1071     return;
1072   }
1073 }
1074 
1075 // initialize a recursive mutex lock in a way that works on both glibc + musl
1076 static int
1077 recursive_lock_init(pthread_mutex_t *lock){
1078 #ifndef __GLIBC__
1079 #define PTHREAD_MUTEX_RECURSIVE_NP PTHREAD_MUTEX_RECURSIVE
1080 #endif
1081   pthread_mutexattr_t attr;
1082   if(pthread_mutexattr_init(&attr)){
1083     return -1;
1084   }
1085   if(pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE_NP)){
1086     pthread_mutexattr_destroy(&attr);
1087     return -1;
1088   }
1089   if(pthread_mutex_init(lock, &attr)){
1090     pthread_mutexattr_destroy(&attr);
1091     return -1;
1092   }
1093   pthread_mutexattr_destroy(&attr);
1094   return 0;
1095 #ifndef __GLIBC__
1096 #undef PTHREAD_MUTEX_RECURSIVE_NP
1097 #endif
1098 }
1099 
1100 ncpixelimpl_e notcurses_check_pixel_support(const notcurses* nc){
1101   return nc->tcache.pixel_implementation;
1102 }
1103 
1104 // the earliest stage of initialization. logging does not yet work; all
1105 // diagostics should be emitted with fprintf(stderr).
1106 //  * validates |opts|, if not NULL
1107 //  * creates and zeroes out the notcurses struct
1108 //  * ensures that we're using ASCII or UTF8 and calls setlocale(3)
1109 //  * checks the environment for NOTCURSES_LOGLEVEL and sets ret->loglevel
1110 //  * writes TERM to the environment, if specified via opts->termtype
1111 //
1112 // iff we're using UTF8, |utf8| will be set to 1. it is otherwise set to 0.
1113 __attribute__ ((nonnull (2))) static notcurses*
1114 notcurses_early_init(const struct notcurses_options* opts, FILE* fp, unsigned* utf8){
1115   if(fwide(fp, 0) > 0){
1116     fprintf(stderr, "Error: output stream is wide-oriented\n");
1117     return NULL;
1118   }
1119   notcurses* ret = malloc(sizeof(*ret));
1120   if(ret == NULL){
1121     return ret;
1122   }
1123   memset(ret, 0, sizeof(*ret));
1124   if(opts){
1125     if(opts->flags >= (NCOPTION_DRAIN_INPUT << 1u)){
1126       fprintf(stderr, "Warning: unknown Notcurses options %016" PRIu64 "\n", opts->flags);
1127     }
1128     if(opts->termtype){
1129       if(putenv_term(opts->termtype)){
1130         free(ret);
1131         return NULL;
1132       }
1133     }
1134     ret->flags = opts->flags;
1135     ret->margin_t = opts->margin_t;
1136     ret->margin_b = opts->margin_b;
1137     ret->margin_l = opts->margin_l;
1138     ret->margin_r = opts->margin_r;
1139     ret->loglevel = opts->loglevel;
1140   }
1141   ret->last_pile = NULL;
1142   ret->rstate.f.buf = NULL;
1143   ret->lastframe = NULL;
1144   set_loglevel_from_env(&ret->loglevel);
1145   if(!(ret->flags & NCOPTION_INHIBIT_SETLOCALE)){
1146     init_lang();
1147   }
1148 //fprintf(stderr, "getenv LC_ALL: %s LC_CTYPE: %s\n", getenv("LC_ALL"), getenv("LC_CTYPE"));
1149   const char* encoding = nl_langinfo(CODESET);
1150   if(encoding && !strcmp(encoding, "UTF-8")){
1151     *utf8 = true;
1152   }else{
1153     *utf8 = false;
1154     if(encoding && (strcmp(encoding, "ANSI_X3.4-1968") &&
1155                           strcmp(encoding, "US-ASCII") &&
1156                           strcmp(encoding, "ASCII"))){
1157       fprintf(stderr, "Encoding (\"%s\") was neither ANSI_X3.4-1968 nor UTF-8, refusing to start\n Did you call setlocale()?\n",
1158               encoding ? encoding : "none found");
1159       free(ret);
1160       return NULL;
1161     }
1162   }
1163   ret->cursory = ret->cursorx = -1;
1164   reset_stats(&ret->stats.s);
1165   reset_stats(&ret->stashed_stats);
1166   ret->ttyfp = fp;
1167   egcpool_init(&ret->pool);
1168   if(ret->loglevel > NCLOGLEVEL_TRACE || ret->loglevel < NCLOGLEVEL_SILENT){
1169     fprintf(stderr, "Invalid loglevel %d\n", ret->loglevel);
1170     free(ret);
1171     return NULL;
1172   }
1173   if(recursive_lock_init(&ret->pilelock)){
1174     fprintf(stderr, "Couldn't initialize pile mutex\n");
1175     free(ret);
1176     return NULL;
1177   }
1178   if(pthread_mutex_init(&ret->stats.lock, NULL)){
1179     pthread_mutex_destroy(&ret->pilelock);
1180     free(ret);
1181     return NULL;
1182   }
1183   return ret;
1184 }
1185 
1186 notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
1187   if(outfp == NULL){
1188     outfp = stdout;
1189   }
1190   unsigned utf8;
1191   notcurses* ret = notcurses_early_init(opts, outfp, &utf8);
1192   if(ret == NULL){
1193     return NULL;
1194   }
1195   // the fbuf is needed by notcurses_stop_minimal, so this must be done
1196   // before registering fatal signal handlers.
1197   if(fbuf_init(&ret->rstate.f)){
1198     pthread_mutex_destroy(&ret->pilelock);
1199     pthread_mutex_destroy(&ret->stats.lock);
1200     free(ret);
1201     return NULL;
1202   }
1203   if(setup_signals(ret, (ret->flags & NCOPTION_NO_QUIT_SIGHANDLERS),
1204                    (ret->flags & NCOPTION_NO_WINCH_SIGHANDLER),
1205                    notcurses_stop_minimal)){
1206     fbuf_free(&ret->rstate.f);
1207     pthread_mutex_destroy(&ret->pilelock);
1208     pthread_mutex_destroy(&ret->stats.lock);
1209     free(ret);
1210     return NULL;
1211   }
1212   // don't set loglevel until we've acquired the signal handler, lest we
1213   // change the loglevel out from under a running instance
1214   loglevel = ret->loglevel;
1215   ret->rstate.logendy = -1;
1216   ret->rstate.logendx = -1;
1217   ret->rstate.x = ret->rstate.y = -1;
1218   int fakecursory = ret->rstate.logendy;
1219   int fakecursorx = ret->rstate.logendx;
1220   int* cursory = ret->flags & NCOPTION_PRESERVE_CURSOR ?
1221                   &ret->rstate.logendy : &fakecursory;
1222   int* cursorx = ret->flags & NCOPTION_PRESERVE_CURSOR ?
1223                   &ret->rstate.logendx : &fakecursorx;
1224   if(interrogate_terminfo(&ret->tcache, ret->ttyfp, utf8,
1225                           ret->flags & NCOPTION_NO_ALTERNATE_SCREEN, 0,
1226                           ret->flags & NCOPTION_NO_FONT_CHANGES,
1227                           cursory, cursorx, &ret->stats,
1228                           ret->margin_l, ret->margin_t,
1229                           ret->margin_r, ret->margin_b,
1230                           ret->flags & NCOPTION_DRAIN_INPUT)){
1231     fbuf_free(&ret->rstate.f);
1232     pthread_mutex_destroy(&ret->pilelock);
1233     pthread_mutex_destroy(&ret->stats.lock);
1234     drop_signals(ret);
1235     free(ret);
1236     return NULL;
1237   }
1238   if(ret->tcache.maxpaletteread > -1){
1239     memcpy(ret->palette.chans, ret->tcache.originalpalette.chans,
1240            sizeof(*ret->palette.chans) * (ret->tcache.maxpaletteread + 1));
1241   }
1242   if((ret->flags & NCOPTION_PRESERVE_CURSOR) ||
1243       (!(ret->flags & NCOPTION_SUPPRESS_BANNERS))){
1244     // the u7 led the queries so that we would get a cursor position
1245     // unaffected by any query spill (unconsumed control sequences). move
1246     // us back to that location, in case there was any such spillage.
1247     if(*cursory < 0 || *cursorx < 0){
1248       unsigned cy, cx;
1249       if(locate_cursor(&ret->tcache, &cy, &cx)){
1250         logwarn("couldn't preserve cursor\n");
1251       }else{
1252         *cursory = cy;
1253         *cursorx = cx;
1254       }
1255     }
1256     if(*cursory >= 0 && *cursorx >= 0){
1257       if(goto_location(ret, &ret->rstate.f, *cursory, *cursorx, NULL)){
1258         goto err;
1259       }
1260     }
1261   }
1262   unsigned dimy, dimx, cgeo, pgeo; // latter two are don't-cares
1263   if(update_term_dimensions(&dimy, &dimx, &ret->tcache, ret->margin_b, &cgeo, &pgeo)){
1264     goto err;
1265   }
1266   if(ncvisual_init(ret->loglevel)){
1267     goto err;
1268   }
1269   ret->stdplane = NULL;
1270   if((ret->stdplane = create_initial_ncplane(ret, dimy, dimx)) == NULL){
1271     logpanic("Couldn't create the initial plane (bad margins?)\n");
1272     goto err;
1273   }
1274   reset_term_attributes(&ret->tcache, &ret->rstate.f);
1275   const char* cinvis = get_escape(&ret->tcache, ESCAPE_CIVIS);
1276   if(cinvis && fbuf_emit(&ret->rstate.f, cinvis) < 0){
1277     free_plane(ret->stdplane);
1278     goto err;
1279   }
1280   const char* pushcolors = get_escape(&ret->tcache, ESCAPE_SAVECOLORS);
1281   if(pushcolors && fbuf_emit(&ret->rstate.f, pushcolors)){
1282     free_plane(ret->stdplane);
1283     goto err;
1284   }
1285   init_banner(ret, &ret->rstate.f);
1286   if(fbuf_flush(&ret->rstate.f, ret->ttyfp) < 0){
1287     free_plane(ret->stdplane);
1288     goto err;
1289   }
1290   if(ret->rstate.logendy >= 0){ // if either is set, both are
1291     if(!(ret->flags & NCOPTION_SUPPRESS_BANNERS) && ret->tcache.ttyfd >= 0){
1292       unsigned uendy, uendx;
1293       if(locate_cursor(&ret->tcache, &uendy, &uendx)){
1294         free_plane(ret->stdplane);
1295         goto err;
1296       }
1297       ret->rstate.logendy = uendy;
1298       ret->rstate.logendx = uendx;
1299     }
1300     if(ret->flags & NCOPTION_PRESERVE_CURSOR){
1301       ncplane_cursor_move_yx(ret->stdplane, ret->rstate.logendy, ret->rstate.logendx);
1302     }
1303   }
1304   if(!(ret->flags & NCOPTION_NO_ALTERNATE_SCREEN)){
1305     // perform an explicit clear since the alternate screen was requested
1306     // (smcup *might* clear, but who knows? and it might not have been
1307     // available in any case).
1308     if(clear_and_home(ret, &ret->tcache, &ret->rstate.f)){
1309       goto err;
1310     }
1311     // no need to reestablish a preserved cursor -- that only affects the
1312     // standard plane, not the physical cursor that was just disrupted.
1313   }
1314   // the sprite clear ought take place within the alternate screen, if it's
1315   // being used.
1316   if(!(ret->flags & NCOPTION_NO_CLEAR_BITMAPS)){
1317     if(sprite_clear_all(&ret->tcache, &ret->rstate.f)){
1318       goto err;
1319     }
1320   }
1321   if(ret->rstate.f.used){
1322     if(fbuf_flush(&ret->rstate.f, ret->ttyfp) < 0){
1323       goto err;
1324     }
1325   }
1326   return ret;
1327 
1328 err:
1329   logpanic("Alas, you will not be going to space today.\n");
1330   notcurses_stop_minimal(ret);
1331   fbuf_free(&ret->rstate.f);
1332   if(ret->tcache.ttyfd >= 0 && ret->tcache.tpreserved){
1333     (void)tcsetattr(ret->tcache.ttyfd, TCSAFLUSH, ret->tcache.tpreserved);
1334     free(ret->tcache.tpreserved);
1335   }
1336   drop_signals(ret);
1337   del_curterm(cur_term);
1338   pthread_mutex_destroy(&ret->stats.lock);
1339   pthread_mutex_destroy(&ret->pilelock);
1340   free(ret);
1341   return NULL;
1342 }
1343 
1344 // updates *pile to point at (*pile)->next, frees all but standard pile/plane
1345 static void
1346 ncpile_drop(notcurses* nc, ncpile** pile){
1347   bool sawstdplane = false;
1348   ncpile* next = (*pile)->next;
1349   ncplane* p = (*pile)->top;
1350   while(p){
1351     ncplane* tmp = p->below;
1352     logdebug("killing plane %p, next is %p\n", p, tmp);
1353     if(nc->stdplane != p){
1354       free_plane(p);
1355     }else{
1356       sawstdplane = true;
1357     }
1358     p = tmp;
1359   }
1360   *pile = next;
1361   if(sawstdplane){
1362     ncplane_pile(nc->stdplane)->top = nc->stdplane;
1363     ncplane_pile(nc->stdplane)->bottom = nc->stdplane;
1364     nc->stdplane->above = nc->stdplane->below = NULL;
1365     nc->stdplane->blist = NULL;
1366   }
1367 }
1368 
1369 // drop all piles and all planes, save the standard plane and its pile
1370 void notcurses_drop_planes(notcurses* nc){
1371   logdebug("we have some planes\n");
1372   pthread_mutex_lock(&nc->pilelock);
1373   ncpile* p = ncplane_pile(nc->stdplane);
1374   ncpile* p0 = p;
1375   do{
1376     ncpile_drop(nc, &p);
1377   }while(p0 != p);
1378   pthread_mutex_unlock(&nc->pilelock);
1379   logdebug("all planes dropped\n");
1380 }
1381 
1382 int notcurses_stop(notcurses* nc){
1383   logdebug("stopping notcurses\n");
1384 //notcurses_debug(nc, stderr);
1385   int ret = 0;
1386   if(nc){
1387     ret |= notcurses_stop_minimal(nc);
1388     // if we were not using the alternate screen, our cursor's wherever we last
1389     // wrote. move it to the furthest place to which it advanced.
1390     if(!get_escape(&nc->tcache, ESCAPE_SMCUP)){
1391       fbuf_reset(&nc->rstate.f);
1392 //fprintf(stderr, "CLOSING TO %d/%d\n", nc->rstate.logendy, nc->rstate.logendx);
1393       goto_location(nc, &nc->rstate.f, nc->rstate.logendy, nc->rstate.logendx, NULL);
1394 //fprintf(stderr, "***"); fflush(stderr);
1395       fbuf_finalize(&nc->rstate.f, stdout);
1396     }
1397     if(nc->stdplane){
1398       notcurses_drop_planes(nc);
1399       logdebug("say goodbye to the standard plane\n");
1400       free_plane(nc->stdplane);
1401       logdebug("get outta here\n");
1402     }
1403     if(nc->tcache.ttyfd >= 0){
1404       ret |= close(nc->tcache.ttyfd);
1405     }
1406     egcpool_dump(&nc->pool);
1407     free(nc->lastframe);
1408     // get any current stats loaded into stash_stats
1409     notcurses_stats_reset(nc, NULL);
1410     if(!(nc->flags & NCOPTION_SUPPRESS_BANNERS)){
1411       summarize_stats(nc);
1412     }
1413 #ifndef __MINGW64__
1414     del_curterm(cur_term);
1415 #endif
1416     ret |= pthread_mutex_destroy(&nc->stats.lock);
1417     ret |= pthread_mutex_destroy(&nc->pilelock);
1418     fbuf_free(&nc->rstate.f);
1419     free_terminfo_cache(&nc->tcache);
1420     free(nc);
1421   }
1422   return ret;
1423 }
1424 
1425 uint64_t ncplane_channels(const ncplane* n){
1426   return n->channels;
1427 }
1428 
1429 uint16_t ncplane_styles(const ncplane* n){
1430   return n->stylemask;
1431 }
1432 
1433 void ncplane_set_channels(ncplane* n, uint64_t channels){
1434   n->channels = channels;
1435 }
1436 
1437 void ncplane_set_fg_default(ncplane* n){
1438   ncchannels_set_fg_default(&n->channels);
1439 }
1440 
1441 uint64_t ncplane_set_fchannel(ncplane* n, uint32_t channel){
1442   return ncchannels_set_fchannel(&n->channels, channel);
1443 }
1444 
1445 uint64_t ncplane_set_bchannel(ncplane* n, uint32_t channel){
1446   return ncchannels_set_bchannel(&n->channels, channel);
1447 }
1448 
1449 void ncplane_set_bg_default(ncplane* n){
1450   ncchannels_set_bg_default(&n->channels);
1451 }
1452 
1453 void ncplane_set_bg_rgb8_clipped(ncplane* n, int r, int g, int b){
1454   ncchannels_set_bg_rgb8_clipped(&n->channels, r, g, b);
1455 }
1456 
1457 int ncplane_set_bg_rgb8(ncplane* n, unsigned r, unsigned g, unsigned b){
1458   return ncchannels_set_bg_rgb8(&n->channels, r, g, b);
1459 }
1460 
1461 void ncplane_set_fg_rgb8_clipped(ncplane* n, int r, int g, int b){
1462   ncchannels_set_fg_rgb8_clipped(&n->channels, r, g, b);
1463 }
1464 
1465 int ncplane_set_fg_rgb8(ncplane* n, unsigned r, unsigned g, unsigned b){
1466   return ncchannels_set_fg_rgb8(&n->channels, r, g, b);
1467 }
1468 
1469 int ncplane_set_fg_rgb(ncplane* n, unsigned channel){
1470   return ncchannels_set_fg_rgb(&n->channels, channel);
1471 }
1472 
1473 int ncplane_set_bg_rgb(ncplane* n, unsigned channel){
1474   return ncchannels_set_bg_rgb(&n->channels, channel);
1475 }
1476 
1477 int ncplane_set_fg_alpha(ncplane* n, int alpha){
1478   return ncchannels_set_fg_alpha(&n->channels, alpha);
1479 }
1480 
1481 int ncplane_set_bg_alpha(ncplane *n, int alpha){
1482   return ncchannels_set_bg_alpha(&n->channels, alpha);
1483 }
1484 
1485 int ncplane_set_fg_palindex(ncplane* n, int idx){
1486   return ncchannels_set_fg_palindex(&n->channels, idx);
1487 }
1488 
1489 int ncplane_set_bg_palindex(ncplane* n, int idx){
1490   return ncchannels_set_bg_palindex(&n->channels, idx);
1491 }
1492 
1493 int ncplane_set_base_cell(ncplane* ncp, const nccell* c){
1494   if(nccell_wide_right_p(c)){
1495     return -1;
1496   }
1497   return nccell_duplicate(ncp, &ncp->basecell, c);
1498 }
1499 
1500 int ncplane_set_base(ncplane* ncp, const char* egc, uint16_t stylemask, uint64_t channels){
1501   return nccell_prime(ncp, &ncp->basecell, egc, stylemask, channels);
1502 }
1503 
1504 int ncplane_base(ncplane* ncp, nccell* c){
1505   return nccell_duplicate(ncp, c, &ncp->basecell);
1506 }
1507 
1508 const char* nccell_extended_gcluster(const ncplane* n, const nccell* c){
1509   if(cell_simple_p(c)){
1510     return (const char*)&c->gcluster;
1511   }
1512   return egcpool_extended_gcluster(&n->pool, c);
1513 }
1514 
1515 // 'n' ends up above 'above'
1516 int ncplane_move_above(ncplane* restrict n, ncplane* restrict above){
1517   if(n == above){ // probably gets optimized out =/
1518     return -1;
1519   }
1520   ncpile* p = ncplane_pile(n);
1521   if(above == NULL){
1522     if(n->below){
1523       if( (n->below->above = n->above) ){
1524         n->above->below = n->below;
1525       }else{
1526         p->top = n->below;
1527       }
1528       n->below = NULL;
1529       if( (n->above = p->bottom) ){
1530         n->above->below = n;
1531       }
1532       p->bottom = n;
1533     }
1534     return 0;
1535   }
1536   if(n->below != above){
1537     if(p != ncplane_pile(above)){ // can't move among piles
1538       return -1;
1539     }
1540     // splice out 'n'
1541     if(n->below){
1542       n->below->above = n->above;
1543     }else{
1544       p->bottom = n->above;
1545     }
1546     if(n->above){
1547       n->above->below = n->below;
1548     }else{
1549       p->top = n->below;
1550     }
1551     if( (n->above = above->above) ){
1552       above->above->below = n;
1553     }else{
1554       p->top = n;
1555     }
1556     above->above = n;
1557     n->below = above;
1558   }
1559   return 0;
1560 }
1561 
1562 // 'n' ends up below 'below', or on top if 'below' == NULL
1563 int ncplane_move_below(ncplane* restrict n, ncplane* restrict below){
1564   if(n == below){ // probably gets optimized out =/
1565     return -1;
1566   }
1567   ncpile* p = ncplane_pile(n);
1568   if(below == NULL){
1569     if(n->above){
1570       if( (n->above->below = n->below) ){
1571         n->below->above = n->above;
1572       }else{
1573         p->bottom = n->above;
1574       }
1575       n->above = NULL;
1576       if( (n->below = p->top) ){
1577         n->below->above = n;
1578       }
1579       p->top = n;
1580     }
1581     return 0;
1582   }
1583   if(n->above != below){
1584     if(p != ncplane_pile(below)){ // can't move among piles
1585       return -1;
1586     }
1587     if(n->below){
1588       n->below->above = n->above;
1589     }else{
1590       p->bottom = n->above;
1591     }
1592     if(n->above){
1593       n->above->below = n->below;
1594     }else{
1595       p->top = n->below;
1596     }
1597     if( (n->below = below->below) ){
1598       below->below->above = n;
1599     }else{
1600       p->bottom = n;
1601     }
1602     below->below = n;
1603     n->above = below;
1604   }
1605   return 0;
1606 }
1607 
1608 // if above is NULL, we're moving to the bottom
1609 int ncplane_move_family_above(ncplane* restrict n, ncplane* restrict bpoint){
1610   ncplane* above = ncplane_above(n);
1611   ncplane* below = ncplane_below(n);
1612   if(ncplane_move_above(n, bpoint)){
1613     return -1;
1614   }
1615   // traverse the planes above n, until we hit NULL. do the planes above n
1616   // first, so that we know the topmost element of our new ensplicification.
1617   // at this point, n is the bottommost plane, and we're inserting above it.
1618   ncplane* targ = n;
1619   while(above){
1620     ncplane* tmp = ncplane_above(above);
1621     if(ncplane_descendant_p(above, n)){
1622       ncplane_move_above(above, targ);
1623       targ = above;
1624     }
1625     above = tmp;
1626   }
1627   // n remains the topmost plane, and we're inserting above it. we have to be
1628   // careful this time not to cross into any we moved below n.
1629   const ncplane* topmost = targ;
1630   targ = n;
1631   while(below && below != topmost){
1632     ncplane* tmp = ncplane_below(below);
1633     if(ncplane_descendant_p(below, n)){
1634       ncplane_move_below(below, targ);
1635       targ = below;
1636     }
1637     below = tmp;
1638   }
1639   return 0;
1640 }
1641 
1642 // if below is NULL, we're moving to the top
1643 int ncplane_move_family_below(ncplane* restrict n, ncplane* restrict bpoint){
1644   ncplane* below = ncplane_below(n);
1645   ncplane* above = ncplane_above(n);
1646   if(ncplane_move_below(n, bpoint)){
1647     return -1;
1648   }
1649   // traverse the planes below n, until we hit NULL. do the planes below n
1650   // first, so that we know the bottommost element of our new ensplicification.
1651   // we're inserting below n...
1652   ncplane* targ = n;
1653   while(below){
1654     ncplane* tmp = ncplane_below(below);
1655     if(ncplane_descendant_p(below, n)){
1656       ncplane_move_below(below, targ);
1657       targ = below;
1658     }
1659     below = tmp;
1660   }
1661   // n remains the topmost plane, and we're inserting above it. we have to be
1662   // careful this time not to cross into any we moved below n.
1663   const ncplane* bottommost = targ;
1664   targ = n;
1665   while(above && above != bottommost){
1666     ncplane* tmp = ncplane_above(above);
1667     if(ncplane_descendant_p(above, n)){
1668       ncplane_move_above(above, targ);
1669       targ = above;
1670     }
1671     above = tmp;
1672   }
1673   return 0;
1674 }
1675 
1676 void ncplane_cursor_yx(const ncplane* n, unsigned* y, unsigned* x){
1677   if(y){
1678     *y = n->y;
1679   }
1680   if(x){
1681     *x = n->x;
1682   }
1683 }
1684 
1685 static inline void
1686 nccell_obliterate(ncplane* n, nccell* c){
1687   nccell_release(n, c);
1688   nccell_init(c);
1689 }
1690 
1691 // increment y by 1 and rotate the framebuffer up one line. x moves to 0. any
1692 // non-fixed bound planes move up 1 line if they intersect the plane.
1693 void scroll_down(ncplane* n){
1694 //fprintf(stderr, "pre-scroll: %d/%d %d/%d log: %d scrolling: %u\n", n->y, n->x, n->leny, n->lenx, n->logrow, n->scrolling);
1695   n->x = 0;
1696   if(n->y == n->leny - 1){
1697     if(n->autogrow){
1698       ncplane_resize_simple(n, n->leny + 1, n->lenx);
1699       ncplane_cursor_move_yx(n, n->leny - 1, 0);
1700       return;
1701     }
1702     if(n == notcurses_stdplane(ncplane_notcurses(n))){
1703       ncplane_pile(n)->scrolls++;
1704     }
1705     n->logrow = (n->logrow + 1) % n->leny;
1706     nccell* row = n->fb + nfbcellidx(n, n->y, 0);
1707     for(unsigned clearx = 0 ; clearx < n->lenx ; ++clearx){
1708       nccell_release(n, &row[clearx]);
1709     }
1710     memset(row, 0, sizeof(*row) * n->lenx);
1711     for(struct ncplane* c = n->blist ; c ; c = c->bnext){
1712       if(!c->fixedbound){
1713         if(ncplanes_intersect_p(n, c)){
1714           ncplane_move_rel(c, -1, 0);
1715         }
1716       }
1717     }
1718   }else{
1719     ++n->y;
1720   }
1721 }
1722 
1723 int ncplane_scrollup(ncplane* n, int r){
1724   if(!ncplane_scrolling_p(n)){
1725     logerror("can't scroll %d on non-scrolling plane\n", r);
1726     return -1;
1727   }
1728   if(r < 0){
1729     logerror("can't scroll %d lines\n", r);
1730     return -1;
1731   }
1732   while(r-- > 0){
1733     scroll_down(n);
1734   }
1735   return 0;
1736 }
1737 
1738 // Scroll |n| up until |child| is no longer hidden beneath it. Returns an
1739 // error if |child| is not a child of |n|, or |n| is not scrolling, or |child|
1740 // is fixed. Returns the number of scrolling events otherwise (might be 0).
1741 int ncplane_scrollup_child(ncplane* n, const ncplane* child){
1742   if(ncplane_parent_const(child) != n){
1743     logerror("not a child of specified plane\n");
1744     return -1;
1745   }
1746   if(child->fixedbound){
1747     logerror("child plane is fixed\n");
1748     return -1;
1749   }
1750   int parend = ncplane_abs_y(n) + ncplane_dim_y(n) - 1; // where parent ends
1751   int chend = ncplane_abs_y(child) + ncplane_dim_y(child) - 1; // where child ends
1752   if(chend <= parend){
1753     return 0;
1754   }
1755   int r = chend - parend; // how many rows we need scroll parent
1756   ncplane_cursor_move_yx(n, ncplane_dim_y(n) - 1, 0);
1757   int ret = ncplane_scrollup(n, r);
1758   return ret;
1759 }
1760 
1761 int nccell_load(ncplane* n, nccell* c, const char* gcluster){
1762   int cols;
1763   int bytes = utf8_egc_len(gcluster, &cols);
1764   return pool_load_direct(&n->pool, c, gcluster, bytes, cols);
1765 }
1766 
1767 // where the magic happens. write the single EGC completely described by |egc|,
1768 // occupying |cols| columns, to the ncplane |n| at the coordinate |y|, |x|. if
1769 // either or both of |y|/|x| is -1, the current cursor location for that
1770 // dimension will be used. if the glyph cannot fit on the current line, it is
1771 // an error unless scrolling is enabled.
1772 static int
1773 ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
1774             uint16_t stylemask, uint64_t channels, int bytes){
1775   if(n->sprite){
1776     logerror("can't write [%s] to sprixelated plane\n", egc);
1777     return -1;
1778   }
1779   // reject any control character for output other than newline (and then only
1780   // on a scrolling plane).
1781   if(*egc == '\n'){
1782     // if we're not scrolling, autogrow would be to the right (as opposed to
1783     // down), and thus it still wouldn't apply to the case of a newline.
1784     if(!n->scrolling){
1785       logerror("rejecting newline on non-scrolling plane\n");
1786       return -1;
1787     }
1788   }else if(is_control_egc((const unsigned char*)egc, bytes)){
1789     logerror("rejecting %dB control character\n", bytes);
1790     return -1;
1791   }
1792   // check *before ncplane_cursor_move_yx()* whether we're past the end of the
1793   // line. if scrolling is enabled, move to the next line if so. if x or y are
1794   // specified, we must always try to print at exactly that location, and
1795   // there's no need to check the present location in that dimension.
1796   bool linesend = false;
1797   if(x < 0){
1798     // we checked x for all negatives, but only -1 is valid (our else clause is
1799     // predicated on a non-negative x).
1800     if(x == -1){
1801       if(n->x + cols - 1 >= n->lenx){
1802         linesend = true;
1803       }
1804     }
1805   }else{
1806     if((unsigned)x + cols - 1 >= n->lenx){
1807       linesend = true;
1808     }
1809   }
1810   if(linesend){
1811     if(n->scrolling){
1812       scroll_down(n);
1813     }else if(n->autogrow){
1814       ncplane_resize_simple(n, n->leny, n->lenx + cols);
1815     }else{
1816       logerror("target x %d [%.*s] > length %d\n", n->x, bytes, egc, n->lenx);
1817       return -1;
1818     }
1819   }
1820   // targets outside the plane will be rejected here.
1821   if(ncplane_cursor_move_yx(n, y, x)){
1822     return -1;
1823   }
1824   if(*egc == '\n'){
1825     scroll_down(n);
1826   }
1827   // A wide character obliterates anything to its immediate right (and marks
1828   // that cell as wide). Any character placed atop one cell of a wide character
1829   // obliterates all cells. Note that a two-cell glyph can thus obliterate two
1830   // other two-cell glyphs, totalling four columns.
1831   nccell* targ = ncplane_cell_ref_yx(n, n->y, n->x);
1832   // we're always starting on the leftmost cell of our output glyph. check the
1833   // target, and find the leftmost cell of the glyph it will be displacing.
1834   // obliterate as we go along.
1835   int idx = n->x;
1836   nccell* lmc = targ;
1837   while(nccell_wide_right_p(lmc)){
1838     nccell_obliterate(n, &n->fb[nfbcellidx(n, n->y, idx)]);
1839     lmc = ncplane_cell_ref_yx(n, n->y, --idx);
1840   }
1841   // we're now on the leftmost cell of the target glyph.
1842   int twidth = nccell_cols(lmc);
1843   nccell_release(n, lmc);
1844   twidth -= n->x - idx;
1845   while(--twidth > 0){
1846     nccell_obliterate(n, &n->fb[nfbcellidx(n, n->y, n->x + twidth)]);
1847   }
1848   targ->stylemask = stylemask;
1849   targ->channels = channels;
1850   if(cell_load_direct(n, targ, egc, bytes, cols) < 0){
1851     return -1;
1852   }
1853 //fprintf(stderr, "%08x %016lx %c %d %d\n", targ->gcluster, targ->channels, nccell_double_wide_p(targ) ? 'D' : 'd', bytes, cols);
1854   // must set our right hand sides wide, and check for further damage
1855   if(*egc != '\n'){
1856     ++n->x;
1857     for(int i = 1 ; i < cols ; ++i){
1858       nccell* candidate = &n->fb[nfbcellidx(n, n->y, n->x)];
1859       int off = nccell_cols(candidate);
1860       nccell_release(n, &n->fb[nfbcellidx(n, n->y, n->x)]);
1861       while(--off > 0){
1862         nccell_obliterate(n, &n->fb[nfbcellidx(n, n->y, n->x + off)]);
1863       }
1864       candidate->channels = targ->channels;
1865       candidate->stylemask = targ->stylemask;
1866       candidate->width = targ->width;
1867       ++n->x;
1868     }
1869   }
1870   return cols;
1871 }
1872 
1873 int ncplane_putc_yx(ncplane* n, int y, int x, const nccell* c){
1874   const int cols = nccell_cols(c);
1875   // unfortunately, |c| comes from |n|. the act of writing |c| to |n| could
1876   // grow |n|'s egcpool, invalidating the reference represented by
1877   // nccell_extended_gcluster(). so we must copy and free it.
1878   char* egc = nccell_strdup(n, c);
1879   if(egc == NULL){
1880     logerror("coudln't duplicate cell\n");
1881     return -1;
1882   }
1883   int r = ncplane_put(n, y, x, egc, cols, c->stylemask, c->channels, strlen(egc));
1884   free(egc);
1885   return r;
1886 }
1887 
1888 int ncplane_putegc_yx(ncplane* n, int y, int x, const char* gclust, size_t* sbytes){
1889   int cols;
1890   int bytes = utf8_egc_len(gclust, &cols);
1891   if(bytes < 0){
1892     return -1;
1893   }
1894   if(sbytes){
1895     *sbytes = bytes;
1896   }
1897 //fprintf(stderr, "glust: %s cols: %d wcs: %d\n", gclust, cols, bytes);
1898   return ncplane_put(n, y, x, gclust, cols, n->stylemask, n->channels, bytes);
1899 }
1900 
1901 int ncplane_putchar_stained(ncplane* n, char c){
1902   uint64_t channels = n->channels;
1903   uint16_t stylemask = n->stylemask;
1904   const nccell* targ = &n->fb[nfbcellidx(n, n->y, n->x)];
1905   n->channels = targ->channels;
1906   n->stylemask = targ->stylemask;
1907   int ret = ncplane_putchar(n, c);
1908   n->channels = channels;
1909   n->stylemask = stylemask;
1910   return ret;
1911 }
1912 
1913 int ncplane_putwegc_stained(ncplane* n, const wchar_t* gclust, size_t* sbytes){
1914   uint64_t channels = n->channels;
1915   uint16_t stylemask = n->stylemask;
1916   const nccell* targ = &n->fb[nfbcellidx(n, n->y, n->x)];
1917   n->channels = targ->channels;
1918   n->stylemask = targ->stylemask;
1919   int ret = ncplane_putwegc(n, gclust, sbytes);
1920   n->channels = channels;
1921   n->stylemask = stylemask;
1922   return ret;
1923 }
1924 
1925 int ncplane_putegc_stained(ncplane* n, const char* gclust, size_t* sbytes){
1926   uint64_t channels = n->channels;
1927   uint16_t stylemask = n->stylemask;
1928   const nccell* targ = &n->fb[nfbcellidx(n, n->y, n->x)];
1929   n->channels = targ->channels;
1930   n->stylemask = targ->stylemask;
1931   int ret = ncplane_putegc(n, gclust, sbytes);
1932   n->channels = channels;
1933   n->stylemask = stylemask;
1934   return ret;
1935 }
1936 
1937 int ncplane_cursor_at(const ncplane* n, nccell* c, char** gclust){
1938   if(n->y >= n->leny || n->x >= n->lenx){
1939     return -1;
1940   }
1941   const nccell* src = &n->fb[nfbcellidx(n, n->y, n->x)];
1942   memcpy(c, src, sizeof(*src));
1943   if(cell_simple_p(c)){
1944     *gclust = NULL;
1945   }else if((*gclust = strdup(nccell_extended_gcluster(n, src))) == NULL){
1946     return -1;
1947   }
1948   return 0;
1949 }
1950 
1951 uint16_t notcurses_supported_styles(const notcurses* nc){
1952   return term_supported_styles(&nc->tcache);
1953 }
1954 
1955 unsigned notcurses_palette_size(const notcurses* nc){
1956   return nc->tcache.caps.colors;
1957 }
1958 
1959 char* notcurses_detected_terminal(const notcurses* nc){
1960   return termdesc_longterm(&nc->tcache);
1961 }
1962 
1963 // conform to the specified stylebits
1964 void ncplane_set_styles(ncplane* n, unsigned stylebits){
1965   n->stylemask = (stylebits & NCSTYLE_MASK);
1966 }
1967 
1968 // turn on any specified stylebits
1969 void ncplane_on_styles(ncplane* n, unsigned stylebits){
1970   n->stylemask |= (stylebits & NCSTYLE_MASK);
1971 }
1972 
1973 // turn off any specified stylebits
1974 void ncplane_off_styles(ncplane* n, unsigned stylebits){
1975   n->stylemask &= ~(stylebits & NCSTYLE_MASK);
1976 }
1977 
1978 // i hate the big allocation and two copies here, but eh what you gonna do?
1979 // well, for one, we don't need the huge allocation FIXME
1980 char* ncplane_vprintf_prep(const char* format, va_list ap){
1981   const size_t size = BUFSIZ; // healthy estimate, can embiggen below
1982   char* buf = malloc(size);
1983   if(buf == NULL){
1984     return NULL;
1985   }
1986   va_list vacopy;
1987   va_copy(vacopy, ap);
1988   int ret = vsnprintf(buf, size, format, ap);
1989   if(ret < 0){
1990     free(buf);
1991     va_end(vacopy);
1992     return NULL;
1993   }
1994   if((size_t)ret >= size){
1995     char* tmp = realloc(buf, ret + 1);
1996     if(tmp == NULL){
1997       free(buf);
1998       va_end(vacopy);
1999       return NULL;
2000     }
2001     buf = tmp;
2002     vsprintf(buf, format, vacopy);
2003   }
2004   va_end(vacopy);
2005   return buf;
2006 }
2007 
2008 int ncplane_vprintf_yx(ncplane* n, int y, int x, const char* format, va_list ap){
2009   char* r = ncplane_vprintf_prep(format, ap);
2010   if(r == NULL){
2011     return -1;
2012   }
2013   int ret = ncplane_putstr_yx(n, y, x, r);
2014   free(r);
2015   return ret;
2016 }
2017 
2018 int ncplane_vprintf_aligned(ncplane* n, int y, ncalign_e align,
2019                             const char* format, va_list ap){
2020   char* r = ncplane_vprintf_prep(format, ap);
2021   if(r == NULL){
2022     return -1;
2023   }
2024   int ret = ncplane_putstr_aligned(n, y, align, r);
2025   free(r);
2026   return ret;
2027 }
2028 
2029 int ncplane_vprintf_stained(struct ncplane* n, const char* format, va_list ap){
2030   char* r = ncplane_vprintf_prep(format, ap);
2031   if(r == NULL){
2032     return -1;
2033   }
2034   int ret = ncplane_putstr_stained(n, r);
2035   free(r);
2036   return ret;
2037 }
2038 
2039 int ncplane_putnstr_aligned(struct ncplane* n, int y, ncalign_e align, size_t s, const char* str){
2040   char* chopped = strndup(str, s);
2041   int ret = ncplane_putstr_aligned(n, y, align, chopped);
2042   free(chopped);
2043   return ret;
2044 }
2045 
2046 int ncplane_hline_interp(ncplane* n, const nccell* c, unsigned len,
2047                          uint64_t c1, uint64_t c2){
2048   if(len <= 0){
2049     logerror("passed invalid length %u\n", len);
2050     return -1;
2051   }
2052   unsigned ur, ug, ub;
2053   int r1, g1, b1, r2, g2, b2;
2054   int br1, bg1, bb1, br2, bg2, bb2;
2055   ncchannels_fg_rgb8(c1, &ur, &ug, &ub);
2056   r1 = ur; g1 = ug; b1 = ub;
2057   ncchannels_fg_rgb8(c2, &ur, &ug, &ub);
2058   r2 = ur; g2 = ug; b2 = ub;
2059   ncchannels_bg_rgb8(c1, &ur, &ug, &ub);
2060   br1 = ur; bg1 = ug; bb1 = ub;
2061   ncchannels_bg_rgb8(c2, &ur, &ug, &ub);
2062   br2 = ur; bg2 = ug; bb2 = ub;
2063   int deltr = r2 - r1;
2064   int deltg = g2 - g1;
2065   int deltb = b2 - b1;
2066   int deltbr = br2 - br1;
2067   int deltbg = bg2 - bg1;
2068   int deltbb = bb2 - bb1;
2069   unsigned ret;
2070   nccell dupc = NCCELL_TRIVIAL_INITIALIZER;
2071   if(nccell_duplicate(n, &dupc, c) < 0){
2072     return -1;
2073   }
2074   bool fgdef = false, bgdef = false;
2075   if(ncchannels_fg_default_p(c1) && ncchannels_fg_default_p(c2)){
2076     fgdef = true;
2077   }
2078   if(ncchannels_bg_default_p(c1) && ncchannels_bg_default_p(c2)){
2079     bgdef = true;
2080   }
2081   for(ret = 0 ; ret < len ; ++ret){
2082     int r = (deltr * (int)ret) / (int)len + r1;
2083     int g = (deltg * (int)ret) / (int)len + g1;
2084     int b = (deltb * (int)ret) / (int)len + b1;
2085     int br = (deltbr * (int)ret) / (int)len + br1;
2086     int bg = (deltbg * (int)ret) / (int)len + bg1;
2087     int bb = (deltbb * (int)ret) / (int)len + bb1;
2088     if(!fgdef){
2089       nccell_set_fg_rgb8(&dupc, r, g, b);
2090     }
2091     if(!bgdef){
2092       nccell_set_bg_rgb8(&dupc, br, bg, bb);
2093     }
2094     if(ncplane_putc(n, &dupc) <= 0){
2095       return -1;
2096     }
2097   }
2098   nccell_release(n, &dupc);
2099   return ret;
2100 }
2101 
2102 int ncplane_vline_interp(ncplane* n, const nccell* c, unsigned len,
2103                          uint64_t c1, uint64_t c2){
2104   if(len <= 0){
2105     logerror("passed invalid length %u\n", len);
2106     return -1;
2107   }
2108   unsigned ur, ug, ub;
2109   int r1, g1, b1, r2, g2, b2;
2110   int br1, bg1, bb1, br2, bg2, bb2;
2111   ncchannels_fg_rgb8(c1, &ur, &ug, &ub);
2112   r1 = ur; g1 = ug; b1 = ub;
2113   ncchannels_fg_rgb8(c2, &ur, &ug, &ub);
2114   r2 = ur; g2 = ug; b2 = ub;
2115   ncchannels_bg_rgb8(c1, &ur, &ug, &ub);
2116   br1 = ur; bg1 = ug; bb1 = ub;
2117   ncchannels_bg_rgb8(c2, &ur, &ug, &ub);
2118   br2 = ur; bg2 = ug; bb2 = ub;
2119   int deltr = (r2 - r1) / ((int)len + 1);
2120   int deltg = (g2 - g1) / ((int)len + 1);
2121   int deltb = (b2 - b1) / ((int)len + 1);
2122   int deltbr = (br2 - br1) / ((int)len + 1);
2123   int deltbg = (bg2 - bg1) / ((int)len + 1);
2124   int deltbb = (bb2 - bb1) / ((int)len + 1);
2125   unsigned ypos, xpos;
2126   unsigned ret;
2127   ncplane_cursor_yx(n, &ypos, &xpos);
2128   nccell dupc = NCCELL_TRIVIAL_INITIALIZER;
2129   if(nccell_duplicate(n, &dupc, c) < 0){
2130     return -1;
2131   }
2132   bool fgdef = false, bgdef = false;
2133   if(ncchannels_fg_default_p(c1) && ncchannels_fg_default_p(c2)){
2134     fgdef = true;
2135   }
2136   if(ncchannels_bg_default_p(c1) && ncchannels_bg_default_p(c2)){
2137     bgdef = true;
2138   }
2139   for(ret = 0 ; ret < len ; ++ret){
2140     if(ncplane_cursor_move_yx(n, ypos + ret, xpos)){
2141       return -1;
2142     }
2143     r1 += deltr;
2144     g1 += deltg;
2145     b1 += deltb;
2146     br1 += deltbr;
2147     bg1 += deltbg;
2148     bb1 += deltbb;
2149     if(!fgdef){
2150       nccell_set_fg_rgb8(&dupc, r1, g1, b1);
2151     }
2152     if(!bgdef){
2153       nccell_set_bg_rgb8(&dupc, br1, bg1, bb1);
2154     }
2155     if(ncplane_putc(n, &dupc) <= 0){
2156       return -1;
2157     }
2158   }
2159   nccell_release(n, &dupc);
2160   return ret;
2161 }
2162 
2163 // we must have at least 2x2, or it's an error
2164 int ncplane_box(ncplane* n, const nccell* ul, const nccell* ur,
2165                 const nccell* ll, const nccell* lr, const nccell* hl,
2166                 const nccell* vl, unsigned ystop, unsigned xstop,
2167                 unsigned ctlword){
2168   unsigned yoff, xoff;
2169   ncplane_cursor_yx(n, &yoff, &xoff);
2170   // must be at least 2x2, with its upper-left corner at the current cursor
2171   if(ystop < yoff + 1){
2172     logerror("ystop (%u) insufficient for yoff (%d)\n", ystop, yoff);
2173     return -1;
2174   }
2175   if(xstop < xoff + 1){
2176     logerror("xstop (%u) insufficient for xoff (%d)\n", xstop, xoff);
2177     return -1;
2178   }
2179   unsigned ymax, xmax;
2180   ncplane_dim_yx(n, &ymax, &xmax);
2181   // must be within the ncplane
2182   if(xstop >= xmax || ystop >= ymax){
2183     logerror("Boundary (%ux%u) beyond plane (%dx%d)\n", ystop, xstop, ymax, xmax);
2184     return -1;
2185   }
2186   unsigned edges;
2187   edges = !(ctlword & NCBOXMASK_TOP) + !(ctlword & NCBOXMASK_LEFT);
2188   if(edges >= box_corner_needs(ctlword)){
2189     if(ncplane_putc(n, ul) < 0){
2190       return -1;
2191     }
2192   }
2193   if(!(ctlword & NCBOXMASK_TOP)){ // draw top border, if called for
2194     if(xstop - xoff >= 2){
2195       if(ncplane_cursor_move_yx(n, yoff, xoff + 1)){
2196         return -1;
2197       }
2198       if(!(ctlword & NCBOXGRAD_TOP)){ // cell styling, hl
2199         if(ncplane_hline(n, hl, xstop - xoff - 1) < 0){
2200           return -1;
2201         }
2202       }else{ // gradient, ul -> ur
2203         if(ncplane_hline_interp(n, hl, xstop - xoff - 1, ul->channels, ur->channels) < 0){
2204           return -1;
2205         }
2206       }
2207     }
2208   }
2209   edges = !(ctlword & NCBOXMASK_TOP) + !(ctlword & NCBOXMASK_RIGHT);
2210   if(edges >= box_corner_needs(ctlword)){
2211     if(ncplane_cursor_move_yx(n, yoff, xstop)){
2212       return -1;
2213     }
2214     if(ncplane_putc(n, ur) < 0){
2215       return -1;
2216     }
2217   }
2218   ++yoff;
2219   // middle rows (vertical lines)
2220   if(yoff < ystop){
2221     if(!(ctlword & NCBOXMASK_LEFT)){
2222       if(ncplane_cursor_move_yx(n, yoff, xoff)){
2223         return -1;
2224       }
2225       if((ctlword & NCBOXGRAD_LEFT)){ // grad styling, ul->ll
2226         if(ncplane_vline_interp(n, vl, ystop - yoff, ul->channels, ll->channels) < 0){
2227           return -1;
2228         }
2229       }else{
2230         if(ncplane_vline(n, vl, ystop - yoff) < 0){
2231           return -1;
2232         }
2233       }
2234     }
2235     if(!(ctlword & NCBOXMASK_RIGHT)){
2236       if(ncplane_cursor_move_yx(n, yoff, xstop)){
2237         return -1;
2238       }
2239       if((ctlword & NCBOXGRAD_RIGHT)){ // grad styling, ur->lr
2240         if(ncplane_vline_interp(n, vl, ystop - yoff, ur->channels, lr->channels) < 0){
2241           return -1;
2242         }
2243       }else{
2244         if(ncplane_vline(n, vl, ystop - yoff) < 0){
2245           return -1;
2246         }
2247       }
2248     }
2249   }
2250   // bottom line
2251   yoff = ystop;
2252   edges = !(ctlword & NCBOXMASK_BOTTOM) + !(ctlword & NCBOXMASK_LEFT);
2253   if(edges >= box_corner_needs(ctlword)){
2254     if(ncplane_cursor_move_yx(n, yoff, xoff)){
2255       return -1;
2256     }
2257     if(ncplane_putc(n, ll) < 0){
2258       return -1;
2259     }
2260   }
2261   if(!(ctlword & NCBOXMASK_BOTTOM)){
2262     if(xstop - xoff >= 2){
2263       if(ncplane_cursor_move_yx(n, yoff, xoff + 1)){
2264         return -1;
2265       }
2266       if(!(ctlword & NCBOXGRAD_BOTTOM)){ // cell styling, hl
2267         if(ncplane_hline(n, hl, xstop - xoff - 1) < 0){
2268           return -1;
2269         }
2270       }else{
2271         if(ncplane_hline_interp(n, hl, xstop - xoff - 1, ll->channels, lr->channels) < 0){
2272           return -1;
2273         }
2274       }
2275     }
2276   }
2277   edges = !(ctlword & NCBOXMASK_BOTTOM) + !(ctlword & NCBOXMASK_RIGHT);
2278   if(edges >= box_corner_needs(ctlword)){
2279     if(ncplane_cursor_move_yx(n, yoff, xstop)){
2280       return -1;
2281     }
2282     if(ncplane_putc(n, lr) < 0){
2283       return -1;
2284     }
2285   }
2286   return 0;
2287 }
2288 
2289 // takes the head of a list of bound planes. performs a DFS on all planes bound
2290 // to 'n', and all planes down-list from 'n', moving all *by* 'dy' and 'dx'.
2291 static void
2292 move_bound_planes(ncplane* n, int dy, int dx){
2293   while(n){
2294     if(n->sprite){
2295       sprixel_movefrom(n->sprite, n->absy, n->absx);
2296     }
2297     n->absy += dy;
2298     n->absx += dx;
2299     move_bound_planes(n->blist, dy, dx);
2300     n = n->bnext;
2301   }
2302 }
2303 
2304 int ncplane_move_yx(ncplane* n, int y, int x){
2305   if(n == ncplane_notcurses(n)->stdplane){
2306     return -1;
2307   }
2308   int dy, dx; // amount moved
2309   if(n->boundto == n){
2310     dy = y - n->absy;
2311     dx = x - n->absx;
2312   }else{
2313     dy = (n->boundto->absy + y) - n->absy;
2314     dx = (n->boundto->absx + x) - n->absx;
2315   }
2316   if(dy || dx){ // don't want to trigger sprixel_movefrom() if unneeded
2317     if(n->sprite){
2318       sprixel_movefrom(n->sprite, n->absy, n->absx);
2319     }
2320     n->absx += dx;
2321     n->absy += dy;
2322     move_bound_planes(n->blist, dy, dx);
2323   }
2324   return 0;
2325 }
2326 
2327 int ncplane_y(const ncplane* n){
2328   if(n->boundto == n){
2329     return n->absy;
2330   }
2331   return n->absy - n->boundto->absy;
2332 }
2333 
2334 int ncplane_x(const ncplane* n){
2335   if(n->boundto == n){
2336     return n->absx;
2337   }
2338   return n->absx - n->boundto->absx;
2339 }
2340 
2341 void ncplane_yx(const ncplane* n, int* y, int* x){
2342   if(y){
2343     *y = ncplane_y(n);
2344   }
2345   if(x){
2346     *x = ncplane_x(n);
2347   }
2348 }
2349 
2350 // special case of ncplane_erase_region()
2351 void ncplane_erase(ncplane* n){
2352   loginfo("erasing %dx%d plane\n", n->leny, n->lenx);
2353   if(n->sprite){
2354     sprixel_hide(n->sprite);
2355     destroy_tam(n);
2356   }
2357   // we must preserve the background, but a pure nccell_duplicate() would be
2358   // wiped out by the egcpool_dump(). do a duplication (to get the stylemask
2359   // and channels), and then reload.
2360   char* egc = nccell_strdup(n, &n->basecell);
2361   memset(n->fb, 0, sizeof(*n->fb) * n->leny * n->lenx);
2362   egcpool_dump(&n->pool);
2363   egcpool_init(&n->pool);
2364   // we need to zero out the EGC before handing this off to nccell_load, but
2365   // we don't want to lose the channels/attributes, so explicit gcluster load.
2366   n->basecell.gcluster = 0;
2367   nccell_load(n, &n->basecell, egc);
2368   free(egc);
2369   n->y = n->x = 0;
2370 }
2371 
2372 int ncplane_erase_region(ncplane* n, int ystart, int xstart, int ylen, int xlen){
2373   if(ystart == -1){
2374     ystart = n->y;
2375   }
2376   if(xstart == -1){
2377     xstart = n->x;
2378   }
2379   if(ystart < 0 || xstart < 0){
2380     logerror("Illegal start of erase (%d, %d)\n", ystart, xstart);
2381     return -1;
2382   }
2383   if(ystart >= (int)ncplane_dim_y(n) || xstart >= (int)ncplane_dim_x(n)){
2384     logerror("Illegal start of erase (%d, %d)\n", ystart, xstart);
2385     return -1;
2386   }
2387   if(xlen < 0){
2388     if(xlen + 1 < -xstart){
2389       xlen = -xstart - 1;
2390     }
2391     xstart = xstart + xlen + 1;
2392     xlen = -xlen;
2393   }else if(xlen == 0){
2394     xstart = 0;
2395     xlen = ncplane_dim_x(n);
2396   }
2397   if(xlen > (int)ncplane_dim_x(n) || xstart + xlen > (int)ncplane_dim_x(n)){
2398     xlen = ncplane_dim_x(n) - xstart;
2399   }
2400   if(ylen < 0){
2401     if(ylen + 1 < -ystart){
2402       ylen = -ystart - 1;
2403     }
2404     ystart = ystart + ylen + 1;
2405     ylen = -ylen;
2406   }else if(ylen == 0){
2407     ystart = 0;
2408     ylen = ncplane_dim_y(n);
2409   }
2410   if(ylen > (int)ncplane_dim_y(n) || ystart + ylen > (int)ncplane_dim_y(n)){
2411     ylen = ncplane_dim_y(n) - ystart;
2412   }
2413   // special-case the full plane erasure, as it's powerfully optimized (O(1))
2414   if(ystart == 0 && xstart == 0 &&
2415       ylen == (int)ncplane_dim_y(n) && xlen == (int)ncplane_dim_x(n)){
2416     int tmpy = n->y; // preserve cursor location
2417     int tmpx = n->x;
2418     ncplane_erase(n);
2419     n->y = tmpy;
2420     n->x = tmpx;
2421     return 0;
2422   }
2423   loginfo("erasing %d/%d - %d/%d\n", ystart, xstart, ystart + ylen, xstart + xlen);
2424   for(int y = ystart ; y < ystart + ylen ; ++y){
2425     for(int x = xstart ; x < xstart + xlen ; ++x){
2426       nccell_release(n, &n->fb[nfbcellidx(n, y, x)]);
2427       nccell_init(&n->fb[nfbcellidx(n, y, x)]);
2428     }
2429   }
2430   return 0;
2431 }
2432 
2433 ncplane* ncpile_top(ncplane* n){
2434   return ncplane_pile(n)->top;
2435 }
2436 
2437 ncplane* ncpile_bottom(ncplane* n){
2438   return ncplane_pile(n)->bottom;
2439 }
2440 
2441 ncplane* ncplane_below(ncplane* n){
2442   return n->below;
2443 }
2444 
2445 ncplane* ncplane_above(ncplane* n){
2446   return n->above;
2447 }
2448 
2449 int notcurses_mice_enable(notcurses* n, unsigned eventmask){
2450   if(mouse_setup(&n->tcache, eventmask)){
2451     return -1;
2452   }
2453   return 0;
2454 }
2455 
2456 ncpalette* ncpalette_new(notcurses* nc){
2457   ncpalette* p = malloc(sizeof(*p));
2458   if(p){
2459     memcpy(p, &nc->palette, sizeof(*p));
2460   }
2461   return p;
2462 }
2463 
2464 int ncpalette_use(notcurses* nc, const ncpalette* p){
2465   int ret = -1;
2466   if(!notcurses_canchangecolor(nc)){
2467     return -1;
2468   }
2469   for(size_t z = 0 ; z < sizeof(p->chans) / sizeof(*p->chans) ; ++z){
2470     if(nc->palette.chans[z] != p->chans[z]){
2471       nc->palette.chans[z] = p->chans[z];
2472       nc->palette_damage[z] = true;
2473     }
2474   }
2475   ret = 0;
2476   return ret;
2477 }
2478 
2479 void ncpalette_free(ncpalette* p){
2480   free(p);
2481 }
2482 
2483 bool ncplane_translate_abs(const ncplane* n, int* restrict y, int* restrict x){
2484   ncplane_translate(ncplane_stdplane_const(n), n, y, x);
2485   if(y){
2486     if(*y < 0){
2487       return false;
2488     }
2489     unsigned yval = *y;
2490     if(yval >= n->leny){
2491       return false;
2492     }
2493   }
2494   if(x){
2495     if(*x < 0){
2496       return false;
2497     }
2498     unsigned xval = *x;
2499     if(xval >= n->lenx){
2500       return false;
2501     }
2502   }
2503   return true;
2504 }
2505 
2506 void ncplane_translate(const ncplane* src, const ncplane* dst,
2507                        int* restrict y, int* restrict x){
2508   if(dst == NULL){
2509     dst = ncplane_stdplane_const(src);
2510   }
2511   if(y){
2512     *y = src->absy - dst->absy + *y;
2513   }
2514   if(x){
2515     *x = src->absx - dst->absx + *x;
2516   }
2517 }
2518 
2519 notcurses* ncplane_notcurses(const ncplane* n){
2520   return ncplane_pile(n)->nc;
2521 }
2522 
2523 const notcurses* ncplane_notcurses_const(const ncplane* n){
2524   return ncplane_pile_const(n)->nc;
2525 }
2526 
2527 int ncplane_abs_y(const ncplane* n){
2528   return n->absy;
2529 }
2530 
2531 int ncplane_abs_x(const ncplane* n){
2532   return n->absx;
2533 }
2534 
2535 void ncplane_abs_yx(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
2536   if(y){
2537     *y = ncplane_abs_y(n);
2538   }
2539   if(x){
2540     *x = ncplane_abs_x(n);
2541   }
2542 }
2543 
2544 ncplane* ncplane_parent(ncplane* n){
2545   return n->boundto;
2546 }
2547 
2548 const ncplane* ncplane_parent_const(const ncplane* n){
2549   return n->boundto;
2550 }
2551 
2552 int ncplane_set_name(ncplane* n, const char* name){
2553   char* copy = name ? strdup(name) : NULL;
2554   if(copy == NULL && name != NULL){
2555     return -1;
2556   }
2557   free(n->name);
2558   n->name = copy;
2559   return 0;
2560 }
2561 
2562 char* ncplane_name(const ncplane* n){
2563   return n->name ? strdup(n->name) : NULL;
2564 }
2565 
2566 void ncplane_set_resizecb(ncplane* n, int(*resizecb)(ncplane*)){
2567   n->resizecb = resizecb;
2568 }
2569 
2570 int (*ncplane_resizecb(const ncplane* n))(ncplane*){
2571   return n->resizecb;
2572 }
2573 
2574 int ncplane_resize_placewithin(ncplane* n){
2575   if(n->boundto == n){
2576     return 0;
2577   }
2578   int absy = ncplane_abs_y(n);
2579   int absx = ncplane_abs_x(n);
2580   int ret = 0;
2581   if(absy + ncplane_dim_y(n) > ncplane_dim_y(n->boundto)){
2582     const int dy = (absy + ncplane_dim_y(n)) - ncplane_dim_y(n->boundto);
2583     logdebug("moving up %d\n", dy);
2584     if(ncplane_move_rel(n, -dy, 0)){
2585       ret = -1;
2586     }
2587     absy = ncplane_abs_y(n);
2588   }
2589   if(absx + ncplane_dim_x(n) > ncplane_dim_x(n->boundto)){
2590     const int dx = ncplane_dim_x(n->boundto) - (absx + ncplane_dim_x(n));
2591     logdebug("moving left %d\n", dx);
2592     if(ncplane_move_rel(n, 0, dx)){
2593       ret = -1;
2594     }
2595     absx = ncplane_abs_x(n);
2596   }
2597   // this will prefer upper-left material if the child plane is larger than
2598   // the parent. we might want a smarter rule, one based on origin?
2599   if(absy < 0){
2600     logdebug("moving down %d\n", -absy);
2601     // we're at least partially above our parent
2602     if(ncplane_move_rel(n, -absy, 0)){
2603       ret = -1;
2604     }
2605     absy = ncplane_abs_y(n);
2606   }
2607   if(absx < 0){
2608     logdebug("moving right %d\n", -absx);
2609     // we're at least partially to the left of our parent
2610     if(ncplane_move_rel(n, 0, -absx)){
2611       ret = -1;
2612     }
2613     absx = ncplane_abs_x(n);
2614   }
2615   return ret;
2616 }
2617 
2618 int ncplane_resize_marginalized(ncplane* n){
2619   const ncplane* parent = ncplane_parent_const(n);
2620   // a marginalized plane cannot be larger than its oppressor plane =]
2621   unsigned maxy, maxx;
2622   if(parent == n){ // root plane, need to use pile size
2623     ncpile* p = ncplane_pile(n);
2624     maxy = p->dimy;
2625     maxx = p->dimx;
2626   }else{
2627     ncplane_dim_yx(parent, &maxy, &maxx);
2628   }
2629   if((maxy -= (n->margin_b + (n->absy - n->boundto->absy))) < 1){
2630     maxy = 1;
2631   }
2632   if((maxx -= (n->margin_r + (n->absx - n->boundto->absx))) < 1){
2633     maxx = 1;
2634   }
2635   unsigned oldy, oldx;
2636   ncplane_dim_yx(n, &oldy, &oldx); // current dimensions of 'n'
2637   unsigned keepleny = oldy > maxy ? maxy : oldy;
2638   unsigned keeplenx = oldx > maxx ? maxx : oldx;
2639   if(ncplane_resize_internal(n, 0, 0, keepleny, keeplenx, 0, 0, maxy, maxx)){
2640     return -1;
2641   }
2642   int targy = maxy - n->margin_b;
2643   int targx = maxx - n->margin_b;
2644   loginfo("marg %d/%d, pdim %d/%d, move %d/%d\n", n->margin_b, n->margin_r, maxy, maxx, targy, targx);
2645   return ncplane_move_yx(n, targy, targx);
2646 }
2647 
2648 int ncplane_resize_maximize(ncplane* n){
2649   const ncpile* pile = ncplane_pile(n); // FIXME should be taken against parent
2650   const unsigned rows = pile->dimy;
2651   const unsigned cols = pile->dimx;
2652   unsigned oldy, oldx;
2653   ncplane_dim_yx(n, &oldy, &oldx); // current dimensions of 'n'
2654   unsigned keepleny = oldy > rows ? rows : oldy;
2655   unsigned keeplenx = oldx > cols ? cols : oldx;
2656   return ncplane_resize_internal(n, 0, 0, keepleny, keeplenx, 0, 0, rows, cols);
2657 }
2658 
2659 int ncplane_resize_realign(ncplane* n){
2660   const ncplane* parent = ncplane_parent_const(n);
2661   if(parent == n){
2662     logerror("Can't realign a root plane\n");
2663     return 0;
2664   }
2665   if(n->halign == NCALIGN_UNALIGNED && n->valign == NCALIGN_UNALIGNED){
2666     logerror("Passed a non-aligned plane\n");
2667     return -1;
2668   }
2669   int xpos = ncplane_x(n);
2670   if(n->halign != NCALIGN_UNALIGNED){
2671     xpos = ncplane_halign(parent, n->halign, ncplane_dim_x(n));
2672   }
2673   int ypos = ncplane_y(n);
2674   if(n->valign != NCALIGN_UNALIGNED){
2675     ypos = ncplane_valign(parent, n->valign, ncplane_dim_y(n));
2676   }
2677   return ncplane_move_yx(n, ypos, xpos);
2678 }
2679 
2680 // The standard plane cannot be reparented; we return NULL in that case.
2681 // If provided |newparent|==|n|, we are moving |n| to its own pile. If |n|
2682 // is already bound to |newparent|, this is a no-op, and we return |n|.
2683 // This is essentially a wrapper around ncplane_reparent_family() that first
2684 // reparents any children to the parent of 'n', or makes them root planes if
2685 // 'n' is a root plane.
2686 ncplane* ncplane_reparent(ncplane* n, ncplane* newparent){
2687   const notcurses* nc = ncplane_notcurses_const(n);
2688   if(n == nc->stdplane){
2689     logerror("won't reparent standard plane\n");
2690     return NULL; // can't reparent standard plane
2691   }
2692   if(n->boundto == newparent){
2693     loginfo("won't reparent plane to itself\n");
2694     return n; // don't return error, just a no-op
2695   }
2696 //notcurses_debug(ncplane_notcurses(n), stderr);
2697   if(n->blist){
2698     if(n->boundto == n){ // children become new root planes
2699       ncplane* lastlink;
2700       ncplane* child = n->blist;
2701       do{
2702         child->boundto = child;
2703         lastlink = child;
2704         child = child->bnext;
2705       }while(child); // n->blist != NULL -> lastlink != NULL
2706       if( (lastlink->bnext = ncplane_pile(n)->roots) ){
2707         lastlink->bnext->bprev = &lastlink->bnext;
2708       }
2709       n->blist->bprev = &ncplane_pile(n)->roots;
2710       ncplane_pile(n)->roots = n->blist;
2711     }else{ // children are rebound to current parent
2712       ncplane* lastlink;
2713       ncplane* child = n->blist;
2714       do{
2715         child->boundto = n->boundto;
2716         lastlink = child;
2717         child = child->bnext;
2718       }while(child); // n->blist != NULL -> lastlink != NULL
2719       if( (lastlink->bnext = n->boundto->blist) ){
2720         lastlink->bnext->bprev = &lastlink->bnext;
2721       }
2722       n->blist->bprev = &n->boundto->blist;
2723       n->boundto->blist = n->blist;
2724     }
2725     n->blist = NULL;
2726   }
2727   // FIXME would be nice to skip ncplane_descendant_p() on this call...:/
2728   return ncplane_reparent_family(n, newparent);
2729 }
2730 
2731 // unsplice self from the z-axis, and then unsplice all children, recursively.
2732 // to be called before unbinding 'n' from old pile.
2733 static void
2734 unsplice_zaxis_recursive(ncplane* n){
2735   // might already have been unspliced, in which case ->above/->below are NULL
2736   if(ncplane_pile(n)->top == n){
2737     ncplane_pile(n)->top = n->below;
2738   }else if(n->above){
2739     n->above->below = n->below;
2740   }
2741   if(ncplane_pile(n)->bottom == n){
2742     ncplane_pile(n)->bottom = n->above;
2743   }else if(n->below){
2744     n->below->above = n->above;
2745   }
2746   for(ncplane* child = n->blist ; child ; child = child->bnext){
2747     unsplice_zaxis_recursive(child);
2748   }
2749   n->below = n->above = NULL;
2750 }
2751 
2752 // unsplice our sprixel from the pile's sprixellist, and then unsplice all
2753 // children, recursively. call before unbinding. returns a doubly-linked
2754 // list of any sprixels found.
2755 static sprixel*
2756 unsplice_sprixels_recursive(ncplane* n, sprixel* prev){
2757   sprixel* s = n->sprite;
2758   if(s){
2759     if(s->prev){
2760       s->prev->next = s->next;
2761     }else{
2762       ncplane_pile(n)->sprixelcache = s->next;
2763     }
2764     if(s->next){
2765       s->next->prev = s->prev;
2766     }
2767     if( (s->prev = prev) ){
2768       prev->next = s;
2769     }
2770     s->next = NULL;
2771     prev = s;
2772   }
2773   for(ncplane* child = n->blist ; child ; child = child->bnext){
2774     unsplice_sprixels_recursive(child, prev);
2775     while(prev && prev->next){ // FIXME lame
2776       prev = prev->next;
2777     }
2778   }
2779   return prev;
2780 }
2781 
2782 // recursively splice 'n' and children into the z-axis, above 'n->boundto'.
2783 // handles 'n' == 'n->boundto'. to be called after binding 'n' into new pile.
2784 static void
2785 splice_zaxis_recursive(ncplane* n, ncpile* p, unsigned ocellpxy, unsigned ocellpxx,
2786                        unsigned ncellpxy, unsigned ncellpxx){
2787   n->pile = p;
2788   if(n != n->boundto){
2789     if((n->above = n->boundto->above) == NULL){
2790       n->pile->top = n;
2791     }else{
2792       n->boundto->above->below = n;
2793     }
2794     n->below = n->boundto;
2795     n->boundto->above = n;
2796   }
2797   if(n->sprite){
2798     if(ocellpxy != ncellpxy || ocellpxx != ncellpxx){
2799       sprixel_rescale(n->sprite, ncellpxy, ncellpxx);
2800       // FIXME do what on error?
2801     }
2802   }
2803   for(ncplane* child = n->blist ; child ; child = child->bnext){
2804     splice_zaxis_recursive(child, p, ocellpxy, ocellpxx, ncellpxy, ncellpxx);
2805   }
2806 }
2807 
2808 ncplane* ncplane_reparent_family(ncplane* n, ncplane* newparent){
2809   // ncplane_notcurses() goes through ncplane_pile(). since we're possibly
2810   // destroying piles below, get the notcurses reference early on.
2811   notcurses* nc = ncplane_notcurses(n);
2812   if(n == nc->stdplane){
2813     return NULL; // can't reparent standard plane
2814   }
2815   if(n->boundto == newparent){ // no-op
2816     return n;
2817   }
2818   if(ncplane_descendant_p(newparent, n)){
2819     return NULL;
2820   }
2821 //notcurses_debug(ncplane_notcurses(n), stderr);
2822   if(n->bprev){ // extract from sibling list
2823     if( (*n->bprev = n->bnext) ){
2824       n->bnext->bprev = n->bprev;
2825     }
2826   }else if(n->bnext){
2827     n->bnext->bprev = NULL;
2828   }
2829   n->bprev = NULL;
2830   n->bnext = NULL;
2831   // if leaving a pile, extract n from the old zaxis, and also any sprixel
2832   sprixel* s = NULL;
2833   if(n == newparent || ncplane_pile(n) != ncplane_pile(newparent)){
2834     unsplice_zaxis_recursive(n);
2835     s = unsplice_sprixels_recursive(n, NULL);
2836   }
2837   const unsigned ocellpxy = ncplane_pile(n)->cellpxy;
2838   const unsigned ocellpxx = ncplane_pile(n)->cellpxx;
2839   n->boundto = newparent;
2840   if(n == n->boundto){ // we're a new root plane
2841     logdebug("reparenting new root plane %p\n", n);
2842     unsplice_zaxis_recursive(n);
2843     n->bnext = NULL;
2844     n->bprev = NULL;
2845     pthread_mutex_lock(&nc->pilelock);
2846     if(ncplane_pile(n)->top == NULL){ // did we just empty our pile?
2847       ncpile_destroy(ncplane_pile(n));
2848     }
2849     make_ncpile(nc, n);
2850     unsigned ncellpxy = ncplane_pile(n)->cellpxy;
2851     unsigned ncellpxx = ncplane_pile(n)->cellpxx;
2852     pthread_mutex_unlock(&nc->pilelock);
2853     if(ncplane_pile(n)){ // FIXME otherwise, we've got a problem...!
2854       splice_zaxis_recursive(n, ncplane_pile(n), ocellpxy, ocellpxx, ncellpxy, ncellpxx);
2855     }
2856   }else{ // establish ourselves as a sibling of new parent's children
2857     if( (n->bnext = newparent->blist) ){
2858       n->bnext->bprev = &n->bnext;
2859     }
2860     n->bprev = &newparent->blist;
2861     newparent->blist = n;
2862     // place it immediately above the new binding plane if crossing piles
2863     if(ncplane_pile(n) != ncplane_pile(n->boundto)){
2864       unsigned ncellpxy = ncplane_pile(n->boundto)->cellpxy;
2865       unsigned ncellpxx = ncplane_pile(n->boundto)->cellpxx;
2866       pthread_mutex_lock(&nc->pilelock);
2867       if(ncplane_pile(n)->top == NULL){ // did we just empty our pile?
2868         ncpile_destroy(ncplane_pile(n));
2869       }
2870       n->pile = ncplane_pile(n->boundto);
2871       pthread_mutex_unlock(&nc->pilelock);
2872       splice_zaxis_recursive(n, ncplane_pile(n), ocellpxy, ocellpxx, ncellpxy, ncellpxx);
2873     }
2874   }
2875   if(s){ // must be on new plane, with sprixels to donate
2876     sprixel* lame = s;
2877     while(lame->next){
2878       lame = lame->next;
2879     }
2880     if( (lame->next = n->pile->sprixelcache) ){
2881       n->pile->sprixelcache->prev = lame;
2882     }
2883     n->pile->sprixelcache = s;
2884   }
2885   return n;
2886 }
2887 
2888 bool ncplane_set_scrolling(ncplane* n, unsigned scrollp){
2889   bool old = n->scrolling;
2890   n->scrolling = scrollp;
2891   return old;
2892 }
2893 
2894 bool ncplane_scrolling_p(const ncplane* n){
2895   return n->scrolling;
2896 }
2897 
2898 bool ncplane_set_autogrow(ncplane* n, unsigned growp){
2899   if(n == notcurses_stdplane_const(ncplane_notcurses_const(n))){
2900     logerror("can't set the standard plane autogrow\n");
2901     return false;
2902   }
2903   bool old = n->autogrow;
2904   n->autogrow = growp;
2905   return old;
2906 }
2907 
2908 bool ncplane_autogrow_p(const ncplane* n){
2909   return n->autogrow;
2910 }
2911 
2912 // extract an integer, which must be non-negative, and followed by either a
2913 // comma or a NUL terminator.
2914 static int
2915 lex_ulong(const char* op, unsigned* i, char** endptr){
2916   errno = 0;
2917   long l = strtol(op, endptr, 10);
2918   if(l < 0 || (l == LONG_MAX && errno == ERANGE) || (l > INT_MAX)){
2919     fprintf(stderr, "Invalid margin: %s\n", op);
2920     return -1;
2921   }
2922   if((**endptr != ',' && **endptr) || *endptr == op){
2923     fprintf(stderr, "Invalid margin: %s\n", op);
2924     return -1;
2925   }
2926   *i = l;
2927   return 0;
2928 }
2929 
2930 int notcurses_lex_scalemode(const char* op, ncscale_e* scalemode){
2931   if(strcasecmp(op, "stretch") == 0){
2932     *scalemode = NCSCALE_STRETCH;
2933   }else if(strcasecmp(op, "scalehi") == 0){
2934     *scalemode = NCSCALE_SCALE_HIRES;
2935   }else if(strcasecmp(op, "hires") == 0){
2936     *scalemode = NCSCALE_NONE_HIRES;
2937   }else if(strcasecmp(op, "scale") == 0){
2938     *scalemode = NCSCALE_SCALE;
2939   }else if(strcasecmp(op, "none") == 0){
2940     *scalemode = NCSCALE_NONE;
2941   }else{
2942     return -1;
2943   }
2944   return 0;
2945 }
2946 
2947 const char* notcurses_str_scalemode(ncscale_e scalemode){
2948   if(scalemode == NCSCALE_STRETCH){
2949     return "stretch";
2950   }else if(scalemode == NCSCALE_SCALE){
2951     return "scale";
2952   }else if(scalemode == NCSCALE_NONE){
2953     return "none";
2954   }else if(scalemode == NCSCALE_NONE_HIRES){
2955     return "hires";
2956   }else if(scalemode == NCSCALE_SCALE_HIRES){
2957     return "scalehi";
2958   }
2959   return NULL;
2960 }
2961 
2962 int notcurses_lex_margins(const char* op, notcurses_options* opts){
2963   char* eptr;
2964   if(lex_ulong(op, &opts->margin_t, &eptr)){
2965     return -1;
2966   }
2967   if(!*eptr){ // allow a single value to be specified for all four margins
2968     opts->margin_r = opts->margin_l = opts->margin_b = opts->margin_t;
2969     return 0;
2970   }
2971   op = ++eptr; // once here, we require four values
2972   if(lex_ulong(op, &opts->margin_r, &eptr) || !*eptr){
2973     return -1;
2974   }
2975   op = ++eptr;
2976   if(lex_ulong(op, &opts->margin_b, &eptr) || !*eptr){
2977     return -1;
2978   }
2979   op = ++eptr;
2980   if(lex_ulong(op, &opts->margin_l, &eptr) || *eptr){ // must end in NUL
2981     return -1;
2982   }
2983   return 0;
2984 }
2985 
2986 int notcurses_inputready_fd(notcurses* n){
2987   return inputready_fd(n->tcache.ictx);
2988 }
2989 
2990 int ncdirect_inputready_fd(ncdirect* n){
2991   return inputready_fd(n->tcache.ictx);
2992 }
2993 
2994 // FIXME speed this up, PoC
2995 // given an egc, get its index in the blitter's EGC set
2996 static int
2997 get_blitter_egc_idx(const struct blitset* bset, const char* egc){
2998   wchar_t wc;
2999   mbstate_t mbs = {};
3000   size_t sret = mbrtowc(&wc, egc, strlen(egc), &mbs);
3001   if(sret == (size_t)-1 || sret == (size_t)-2){
3002     return -1;
3003   }
3004   wchar_t* wptr = wcsrchr(bset->egcs, wc);
3005   if(wptr == NULL){
3006 //fprintf(stderr, "FAILED TO FIND [%s] (%lc) in [%ls]\n", egc, wc, bset->egcs);
3007     return -1;
3008   }
3009 //fprintf(stderr, "FOUND [%s] (%lc) in [%ls] (%zu)\n", egc, wc, bset->egcs, wptr - bset->egcs);
3010   return wptr - bset->egcs;
3011 }
3012 
3013 static bool
3014 is_bg_p(int idx, int py, int px, int width){
3015   // bit increases to the right, and down
3016   const int bpos = py * width + px; // bit corresponding to pixel, 0..|egcs|-1
3017   const unsigned mask = 1u << bpos;
3018   if(idx & mask){
3019     return false;
3020   }
3021   return true;
3022 }
3023 
3024 static inline uint32_t*
3025 ncplane_as_rgba_internal(const ncplane* nc, ncblitter_e blit,
3026                          int begy, int begx, unsigned leny, unsigned lenx,
3027                          unsigned* pxdimy, unsigned* pxdimx){
3028   const notcurses* ncur = ncplane_notcurses_const(nc);
3029   unsigned ystart, xstart;
3030   if(check_geometry_args(nc, begy, begx, &leny, &lenx, &ystart, &xstart)){
3031     return NULL;
3032   }
3033   if(blit == NCBLIT_PIXEL){ // FIXME extend this to support sprixels
3034     logerror("pixel blitter %d not yet supported\n", blit);
3035     return NULL;
3036   }
3037   if(blit == NCBLIT_DEFAULT){
3038     logerror("must specify exact blitter, not NCBLIT_DEFAULT\n");
3039     return NULL;
3040   }
3041   const struct blitset* bset = lookup_blitset(&ncur->tcache, blit, false);
3042   if(bset == NULL){
3043     logerror("blitter %d invalid in current environment\n", blit);
3044     return NULL;
3045   }
3046 //fprintf(stderr, "ALLOCATING %u %d %d %p\n", 4u * lenx * leny * 2, leny, lenx, bset);
3047   if(pxdimy){
3048     *pxdimy = leny * bset->height;
3049   }
3050   if(pxdimx){
3051     *pxdimx = lenx * bset->width;
3052   }
3053   uint32_t* ret = malloc(sizeof(*ret) * lenx * bset->width * leny * bset->height);
3054 //fprintf(stderr, "GEOM: %d/%d %d/%d ret: %p\n", bset->height, bset->width, *pxdimy, *pxdimx, ret);
3055   if(ret){
3056     for(unsigned y = ystart, targy = 0 ; y < ystart + leny ; ++y, targy += bset->height){
3057       for(unsigned x = xstart, targx = 0 ; x < xstart + lenx ; ++x, targx += bset->width){
3058         uint16_t stylemask;
3059         uint64_t channels;
3060         char* c = ncplane_at_yx(nc, y, x, &stylemask, &channels);
3061         if(c == NULL){
3062           free(ret);
3063           return NULL;
3064         }
3065         int idx = get_blitter_egc_idx(bset, c);
3066         if(idx < 0){
3067           free(ret);
3068           free(c);
3069           return NULL;
3070         }
3071         unsigned fr, fg, fb, br, bg, bb, fa, ba;
3072         ncchannels_fg_rgb8(channels, &fr, &fb, &fg);
3073         fa = ncchannels_fg_alpha(channels);
3074         ncchannels_bg_rgb8(channels, &br, &bb, &bg);
3075         ba = ncchannels_bg_alpha(channels);
3076         // handle each destination pixel from this cell
3077         for(unsigned py = 0 ; py < bset->height ; ++py){
3078           for(unsigned px = 0 ; px < bset->width ; ++px){
3079             uint32_t* p = &ret[(targy + py) * (lenx * bset->width) + (targx + px)];
3080             bool background = is_bg_p(idx, py, px, bset->width);
3081             if(background){
3082               if(ba){
3083                 *p = 0;
3084               }else{
3085                 ncpixel_set_a(p, 0xff);
3086                 ncpixel_set_r(p, br);
3087                 ncpixel_set_g(p, bb);
3088                 ncpixel_set_b(p, bg);
3089               }
3090             }else{
3091               if(fa){
3092                 *p = 0;
3093               }else{
3094                 ncpixel_set_a(p, 0xff);
3095                 ncpixel_set_r(p, fr);
3096                 ncpixel_set_g(p, fb);
3097                 ncpixel_set_b(p, fg);
3098               }
3099             }
3100           }
3101         }
3102         free(c);
3103       }
3104     }
3105   }
3106   return ret;
3107 }
3108 
3109 uint32_t* ncplane_as_rgba(const ncplane* nc, ncblitter_e blit,
3110                           int begy, int begx, unsigned leny,
3111                           unsigned lenx, unsigned* pxdimy, unsigned* pxdimx){
3112   unsigned px, py;
3113   if(!pxdimy){
3114     pxdimy = &py;
3115   }
3116   if(!pxdimx){
3117     pxdimx = &px;
3118   }
3119   return ncplane_as_rgba_internal(nc, blit, begy, begx, leny, lenx, pxdimy, pxdimx);
3120 }
3121 
3122 // return a heap-allocated copy of the contents
3123 char* ncplane_contents(ncplane* nc, int begy, int begx, unsigned leny, unsigned lenx){
3124   unsigned ystart, xstart;
3125   if(check_geometry_args(nc, begy, begx, &leny, &lenx, &ystart, &xstart)){
3126     return NULL;
3127   }
3128   size_t retlen = 1;
3129   char* ret = malloc(retlen);
3130   if(ret){
3131     for(unsigned y = ystart, targy = 0 ; y < ystart + leny ; ++y, targy += 2){
3132       for(unsigned x = xstart, targx = 0 ; x < xstart + lenx ; ++x, ++targx){
3133         nccell ncl = NCCELL_TRIVIAL_INITIALIZER;
3134         // we need ncplane_at_yx_cell() here instead of ncplane_at_yx(),
3135         // because we should only have one copy of each wide EGC.
3136         int clen;
3137         if((clen = ncplane_at_yx_cell(nc, y, x, &ncl)) < 0){
3138           free(ret);
3139           return NULL;
3140         }
3141         const char* c = nccell_extended_gcluster(nc, &ncl);
3142         if(clen){
3143           char* tmp = realloc(ret, retlen + clen);
3144           if(!tmp){
3145             free(ret);
3146             return NULL;
3147           }
3148           ret = tmp;
3149           memcpy(ret + retlen - 1, c, clen);
3150           retlen += clen;
3151         }
3152       }
3153     }
3154     ret[retlen - 1] = '\0';
3155   }
3156   return ret;
3157 }
3158 
3159 // find the center coordinate of a plane, preferring the top/left in the
3160 // case of an even number of rows/columns (in such a case, there will be one
3161 // more cell to the bottom/right of the center than the top/left). the
3162 // center is then modified relative to the plane's origin.
3163 void ncplane_center_abs(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
3164   ncplane_center(n, y, x);
3165   if(y){
3166     *y += n->absy;
3167   }
3168   if(x){
3169     *x += n->absx;
3170   }
3171 }
3172 
3173 void nclog(const char* fmt, ...){
3174   va_list va;
3175   va_start(va, fmt);
3176   vfprintf(stderr, fmt, va);
3177   va_end(va);
3178 }
3179 
3180 int ncplane_putwstr_stained(ncplane* n, const wchar_t* gclustarr){
3181   mbstate_t ps = {};
3182   const wchar_t** wset = &gclustarr;
3183   size_t mbytes = wcsrtombs(NULL, wset, 0, &ps);
3184   if(mbytes == (size_t)-1){
3185     logerror("error converting wide string\n");
3186     return -1;
3187   }
3188   ++mbytes;
3189   char* mbstr = malloc(mbytes);
3190   if(mbstr == NULL){
3191     return -1;
3192   }
3193   size_t s = wcsrtombs(mbstr, wset, mbytes, &ps);
3194   if(s == (size_t)-1){
3195     free(mbstr);
3196     return -1;
3197   }
3198   int r = ncplane_putstr_stained(n, mbstr);
3199   free(mbstr);
3200   return r;
3201 }
3202 
3203 int notcurses_ucs32_to_utf8(const uint32_t* ucs32, unsigned ucs32count,
3204                             unsigned char* resultbuf, size_t buflen){
3205   if(u32_to_u8(ucs32, ucs32count, resultbuf, &buflen) == NULL){
3206     return -1;
3207   }
3208   return buflen;
3209 }
3210 
3211 int ncstrwidth(const char* egcs, int* validbytes, int* validwidth){
3212   int cols;
3213   if(validwidth == NULL){
3214     validwidth = &cols;
3215   }
3216   *validwidth = 0;
3217   int bytes;
3218   if(validbytes == NULL){
3219     validbytes = &bytes;
3220   }
3221   *validbytes = 0;
3222   do{
3223     int thesecols, thesebytes;
3224     thesebytes = utf8_egc_len(egcs, &thesecols);
3225     if(thesebytes < 0){
3226       return -1;
3227     }
3228     egcs += thesebytes;
3229     *validbytes += thesebytes;
3230     *validwidth += thesecols;
3231   }while(*egcs);
3232   return *validwidth;
3233 }
3234 
3235 void ncplane_pixel_geom(const ncplane* n,
3236                         unsigned* RESTRICT pxy, unsigned* RESTRICT pxx,
3237                         unsigned* RESTRICT celldimy, unsigned* RESTRICT celldimx,
3238                         unsigned* RESTRICT maxbmapy, unsigned* RESTRICT maxbmapx){
3239   const notcurses* nc = ncplane_notcurses_const(n);
3240   const ncpile* p = ncplane_pile_const(n);
3241   if(celldimy){
3242     *celldimy = p->cellpxy;
3243   }
3244   if(celldimx){
3245     *celldimx = p->cellpxx;
3246   }
3247   if(pxy){
3248     *pxy = p->cellpxy * ncplane_dim_y(n);
3249   }
3250   if(pxx){
3251     *pxx = p->cellpxx * ncplane_dim_x(n);
3252   }
3253   if(notcurses_check_pixel_support(nc) > 0){
3254     if(maxbmapy){
3255       *maxbmapy = p->cellpxy * ncplane_dim_y(n);
3256       if(*maxbmapy > nc->tcache.sixel_maxy && nc->tcache.sixel_maxy){
3257         *maxbmapy = nc->tcache.sixel_maxy;
3258       }
3259     }
3260     if(maxbmapx){
3261       *maxbmapx = p->cellpxx * ncplane_dim_x(n);
3262       if(*maxbmapx > nc->tcache.sixel_maxx && nc->tcache.sixel_maxx){
3263         *maxbmapx = nc->tcache.sixel_maxx;
3264       }
3265     }
3266   }else{
3267     if(maxbmapy){
3268       *maxbmapy = 0;
3269     }
3270     if(maxbmapx){
3271       *maxbmapx = 0;
3272     }
3273   }
3274 }
3275 
3276 const nccapabilities* notcurses_capabilities(const notcurses* n){
3277   return &n->tcache.caps;
3278 }
3279