Repository: dankamongmen/notcurses (language: python)

Issue:
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.
don't sent keyboard modifiers with NCOPTION_DRAIN_INPUT is used
When provided `NCOPTION_DRAIN_INPUT`, don't send kitty keyboard specs or `XTMODKEYS`. there's no need, it bulks up input we're going to throw away, and with the kitty protocol, it ought prevent RELEASE events bleeding out the backend.

this is simple enough that i think we want it for 3.0.
CLI mode always involves at least two rows
Run e.g. `grid` or any other CLI-style program that has no text output. There will be a blank line before the new prompt. Running e.g. `true` or other such tools results in no such blank line, and we oughtn't either.


Discussion / hints:
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.
it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.

it looks like we're not emitting newlines or anything, but simply tacking a move onto the end that takes us too far. it's the very last thing we're emitting. we know we're on 2 at the beginning -- why do we go to 3? hrmm.
ok, `cup` is 1-indexed, and `tput` handles that for us. so we're doing a correct move. hrmm.
in xterm i just noticed that `sgb-full` launched from the top emits no final newline, but if we scroll, one shows up at the bottom. probably the same deal?
yeah. `sgr-full` seems a good test. it gives us an extra blank line if it scrolls, and does not otherwise. it never should.
`ncneofetch`, interestingly, does not seem to suffer from this problem
i'm wondering if this is related to `ncneofetch` scrolling what seems one line short when we're close to the bottom?
```c
[schwarzgerat](130) $ ./cli 
notcurses 2.4.8 on Kitty 0.23.1 (Linux 5.14.15nlb)
64 rows (22px) 132 cols (11px) 1408x1452 rgb+256 colors
gcc-11.2.0 (LE)
terminfo 6.2.20210905 zlib 1.2.11 GPM n/a
avformat 58.76.100 avutil 56.70.100 swscale 5.9.100 avcodec 58.134.100

0 failed renders, 0 failed rasters, 0 refreshes, 0 input errors
RGB emits:elides: def 0:0 fg 0:0 bg 0:0
Cell emits:elides: 0:0 (0.00%) 0.00% 0.00% 0.00%
Bitmap emits:elides: 0:0 (0.00%) 0B (0.00%) SuM: 0 (0.00%)
[schwarzgerat](0) $ 
```

this is the absolute minimum notcurses cli:

```c
int main(void){                                                                                                                     
  struct notcurses_options opts = {                                                                                                 
    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
             NCOPTION_PRESERVE_CURSOR |                                                                                             
             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
             NCOPTION_DRAIN_INPUT,                                                                                                  
  };                                                                                                                                
  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
  if(nc == NULL){                                                                                                                   
    return EXIT_FAILURE;                                                                                                            
  }                                                                                                                                 
  notcurses_stop(nc);                                                                                                               
  return EXIT_SUCCESS;                                                                                                              
}
```
sans banners, we output (from line 2):

`^[[3;1H^[[39;49m^[(B^[[m^[]104^G^[[?25l^[[#P^[[?1002;1006l^[[#Q^[[39;49m^[(B^[[m^[]104^G^[[?1l^[[?12h^[[?25h`
different lines are the exact same output except for that first move. ahhh, it looks like there is a linefeed at the end of our output:

```
[schwarzgerat](1) $ xxd < f
00000000: 1b5b 3634 3b31 481b 5b33 393b 3439 6d1b  .[64;1H.[39;49m.
00000010: 2842 1b5b 6d1b 5d31 3034 071b 5b3f 3235  (B.[m.]104..[?25
00000020: 6c1b 5b23 501b 5b3f 3130 3032 3b31 3030  l.[#P.[?1002;100
00000030: 366c 1b5b 2351 1b5b 3339 3b34 396d 1b28  6l.[#Q.[39;49m.(
00000040: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
00000050: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
[schwarzgerat](0) $ 
```

and `exit(1)` at the end of `notcurses_stop_minimal()` eliminates it.
the following seems to remedy the problem, but i'm sure there's some case i'm not considering that prompted this dubious-looking wart:

```c
     if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
       int targy = nc->rstate.logendy;
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
+      /*if(++targy >= nc->lfdimy){
         fbuf_putc(&nc->rstate.f, '\n');
         --targy;
-      }
+      }*/
```
yeah this has us consuming the emoji line with our closing border in `notcurses-info`. redirecting stderr to a file puts our prompt on the emoji line. what's different about this case? hrmmm, we draw the logo at the end, do we not? nah, same thing happens with xterm.

maybe we just want a newline in `notcurses-info`? is that it?
hah i think that might be it lol
and to make that work, we do need to bring the cursor out at the end...yes, this is the problem. we need an explicit newline at the end of our output to bump `logendy` down.

we've absolutely gotta junk that `++targ` in shutdown. it's bad.
i think this only happens if you inhibit banners...?
> ```c
> int main(void){                                                                                                                     
>   struct notcurses_options opts = {                                                                                                 
>     .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
>              NCOPTION_PRESERVE_CURSOR |                                                                                             
>              NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
>              NCOPTION_DRAIN_INPUT,                                                                                                  
>   };                                                                                                                                
>   struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
>   if(nc == NULL){                                                                                                                   
>     return EXIT_FAILURE;                                                                                                            
>   }                                                                                                                                 
>   notcurses_stop(nc);                                                                                                               
>   return EXIT_SUCCESS;                                                                                                              
> }
> ```

nope, the program above has banners, and prints an empty line.
```
00000140: 7675 7469 6c20 3537 2e38 2e31 3030 2073  vutil 57.8.100 s
00000150: 7773 6361 6c65 2036 2e31 2e31 3030 2061  wscale 6.1.100 a
00000160: 7663 6f64 6563 2035 392e 3132 2e31 3030  vcodec 59.12.100
00000170: 0a1b 5b33 383b 353b 3230 336d 1b28 421b  ..[38;5;203m.(B.
00000180: 5b6d 1b5b 2351 1b5b 3339 3b34 396d 1b28  [m.[#Q.[39;49m.(
00000190: 421b 5b6d 1b5d 3130 3407 1b5b 3f31 6c1b  B.[m.]104..[?1l.
000001a0: 5b3f 3132 681b 5b3f 3235 680a            [?12h.[?25h.
```

why this LF at the end?
targy is 63 in this situation, while lfdimy is...0!?
ok yeah it looks like this is just due to `lfdimy` never being set because `notcurses_render()` was never called. that's at least the case for this trivial PoC.
i might have a fix.
ok, let's stop flailing around and be a bit rigorous.

* alternate screen: we always want to have the cursor where we left when we return to the regular screen. this ought be managed by the terminal, so long as all our cursor movements take place within the alternate screen. if we leave and reenter the alternate screen, just put the cursor back where it was upon reentry (does entering the alternate screen always clear the screen? i think it does...yes, it does. do we need refresh upon reentry, then? what do we do with our render context during a temporary departure to the regular screen?)

* `NOTCURSES_OPTION_PRESERVE_CURSOR`: now that i think about it, this is only relevant to startup, not shutdown. it means that at startup we ought keep the cursor where it is, as opposed to placing it at the origin. this is true independently of whether we use the alternate screen (ought it be? ought we not default to preserving the cursor when not using the alternate screen?).

so the only case we really need handle is when we've not placed the cursor at the end of our generated output, i.e. when we write something, and then move the cursor backwards in the output. and in this case, we just want to go back to wherever the furthest place we generated output is, right? 
so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

i think the problem is that this got mixed up with "last place we wrote", as necessary for `goto_location()`. yeah, i think we need to continue calculating `logendy`/`logendx` as we are, and when they're updated, update the new variables if appropriate. this probably needs be tied tightly in with scrolling, though, since we would want to reset in that case (but not generically on a x=0).
no... `x` and `y` in `rstate` are the current cursor position. `logendy`/`logendx` are for placing of the cursor following each rasterization (presumably in the absence of a visible enabled cursor, which would otherwise override). ok we need trace this entire datapath.
> so if that's all correct, we just need to put it at `logendy`/`logendx`, right? and those ought be updated whenever we emit something absolutely greater than the current values? except following a scroll operation, in which case we ought reset `logendx` to 0? isn't that what we were originally doing, though?

it's a bit more complex than this. we don't want to put the cursor at the last place we wrote; we want to put the cursor one past the last place we wrote (more precisely, immediately past the last glyph we wrote; it might have been more than one column). this is probably best said as: we want the cursor wherever the furthest place our cursor ever was (this accounts for scrolling etc., which ought already have taken place for e.g. a glyph emitted in the lower-right corner).

*this* would be further complicated by the fact that this works differently in the alternative and regular screens, except that `logdimx`/`logdimy` are only relevant for the regular screen, so we'll just always compute that way.

so `logendy`/`logendx` get updated in `goto_location()`, but *also get updated in the rastercore, perhaps even in `term_putc()`*. actually, maybe they don't get updated in `goto_location()` at all.
i think this is The Way, but we need to update wherever we emit output, including scrolling and bitmaps.
the problem with this whole approach is: what happens when we write a '\n' to a plane, and it does *not* cause a physical scroll? like let's say we write 5 lines of data ending with newlines on a 10-line terminal. we very much want the cursor on the next line, but no scroll happens if we start printing from the top, and the '\n' character never gets to us; it strictly affects the plane's virtual cursor location.

i think maybe we need to plumb newlines through to rendering, at the very least in CLI mode?
so yeah. our original concept was, i believe, the correct one: **track the furthest advance of the cursor, accounting for scrolling, and always place the cursor there post-render**.

where this broke down, and where we tried to solve it in other, misguided ways, was the issue of printing newlines to planes. this gets very complicated very quickly, but i think the critical thing is to remember that *a newline is a control, **not** a glyph*. we never admit them to a non-scrolling plane, but on a scrolling plane, it ought:

 * clear everything *on the plane* to the right of the newline emission, and
 * move the cursor down (and to the alignment point)

this can all be handled at output time, virtually (and doing so would presumably resolve #1590). but when the newline is on the standard plane, and the standard plane is scrolling, more is necessary. we cannot just update the plane's virtual cursor; we need update the real cursor (and scroll, if the newline was on the bottom line). the problem we now meet is piping the newline through all the way to rasterization from the output point, and what to do there. because the clearing through the end of the plane applies *only to the plane on which the newline was emitted*, **not** to all planes. there could very well be some other plane to the right of the standard plane's newline, above it on the z-axis (or even below it, in the case of a transparent standard plane), and we would still need to rasterize that.

so recall what i emphasized earlier: newline is a *control*, not a glyph. so this glyph is never going to reach rasterization, or at least needn't via the solved frame. so let's go ahead and encode it onto the plane (we're not doing this currently). update the plane's virtual cursor as we presumably are already doing. this allows a later write to the plane to obliterate the newline, which is the semantic we want (on the other hand, if someone writes *past* the newline, on the same line, it will not be rendered, which might be surprising. we'd need reject any attempt to move the virtual cursor horizontally past the newline without obliterating it to get the proper semantic IMHO, requiring O(1) time but O(N) space. maybe worth it?).

then at render time, we whip up ~~a O(N) array of bools, N==rows, and zero them out~~ a boolean, initialized to 0. whenever we hit a newline, treat it as a nil glyph, but then skip the rest of the line -- solve nothing from further on the line in `paint()`. furthermore, on the standard plane only, if on the bottom line, and we encounter a newline, set the bool to 1. this ought solve #1590 in and of itself, and without a possibly expensive scrub (only for left-aligned text, though =\).

finally, in rasterization, if the array is 1 for the given row, we need effect a newline. what would this mean? hrmmm, given the `paint()` changes from above, i think it all comes down to....if there was a newline on the bottom row, we scroll one. so yeah, we only need O(1) state, not O(N) as i'd thought. ok!

i think this is a complete and rigorous solution. we were running into issues of "losing" `notcurses-info`'s last line because we weren't performing a scroll corresponding to its newline. so we added the newline at the end, but that throws off things which don't want it. this way, we get exactly what we want. delicate and subtle, but i think this gets everything.

Fix patch:
diff --git a/.drone.yml b/.drone.yml
index 14166e736f..a0e2fbd668 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -34,7 +34,7 @@ steps:
     - export TERM=xterm
     - mkdir build
     - cd build
-    - cmake -DCMAKE_BUILD_TYPE=Release -DUSE_DEFLATE=no -DUSE_MULTIMEDIA=oiio -DUSE_QRCODEGEN=on ..
+    - cmake -DCMAKE_BUILD_TYPE=Release -DUSE_DEFLATE=off -DUSE_MULTIMEDIA=oiio -DUSE_QRCODEGEN=on ..
     - make -j2
     - ctest --output-on-failure
     - make install
@@ -45,28 +45,28 @@ steps:
     - LDFLAGS=-L/usr/local/lib CFLAGS=-I/usr/local/include python3 setup.py sdist build install
     - env LD_LIBRARY_PATH=/usr/local/lib ./notcurses-pydemo > /dev/null
     - env LD_LIBRARY_PATH=/usr/local/lib ./ncdirect-pydemo > /dev/null
-#---
-#kind: pipeline
-#type: docker
-#name: ubuntu-jellyfish
-#
-#steps:
-#- name: ubuntu-build
-#  image: dankamongmen/impish:2021-11-14b
-#  commands:
-#    - export LANG=es_ES.UTF-8
-#    - export TERM=xterm
-#    - mkdir build
-#    - cd build
-#    - cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_QRCODEGEN=on -DUSE_MULTIMEDIA=none ..
-#    - make -j2
-#    - ctest --output-on-failure
-#    - make install
-#    - ldconfig
-#    - cd ../cffi
-#    - python3 setup.py sdist build
-#    - cd ../python
-#    - LDFLAGS=-L/usr/local/lib CFLAGS=-I/usr/local/include python3 setup.py sdist build install
+---
+kind: pipeline
+type: docker
+name: ubuntu-jellyfish
+
+steps:
+- name: ubuntu-build
+  image: dankamongmen/impish:2021-11-14b
+  commands:
+    - export LANG=es_ES.UTF-8
+    - export TERM=xterm
+    - mkdir build
+    - cd build
+    - cmake -DCMAKE_BUILD_TYPE=Debug -DUSE_DOCTEST=off -DUSE_QRCODEGEN=on -DUSE_MULTIMEDIA=none ..
+    - make -j2
+    - ctest --output-on-failure
+    - make install
+    - ldconfig
+    - cd ../cffi
+    - python3 setup.py sdist build
+    - cd ../python
+    - LDFLAGS=-L/usr/local/lib CFLAGS=-I/usr/local/include python3 setup.py sdist build install
 ---
 kind: pipeline
 type: docker
diff --git a/data/Windows10Logo.png b/data/Windows10Logo.png
new file mode 100644
index 0000000000..1f456ed107
Binary files /dev/null and b/data/Windows10Logo.png differ
diff --git a/data/freebsd.png b/data/freebsd.png
new file mode 100644
index 0000000000..0f30aa26c7
Binary files /dev/null and b/data/freebsd.png differ
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index dc7a3d57c9..10335593ce 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -2032,7 +2032,7 @@ ncplane_putstr_yx(struct ncplane* n, int y, int x, const char* gclusters){
   while(*gclusters){
     size_t wcs;
     int cols = ncplane_putegc_yx(n, y, x, gclusters, &wcs);
-//fprintf(stderr, "wrote %.*s %d cols %d bytes now at %d/%d\n", wcs, gclusters, cols, wcs, n->y, n->x);
+//fprintf(stderr, "wrote %.*s %d cols %zu bytes\n", (int)wcs, gclusters, cols, wcs);
     if(cols < 0){
       return -ret;
     }
diff --git a/src/demo/xray.c b/src/demo/xray.c
index 12117cf8a0..55648aa582 100644
--- a/src/demo/xray.c
+++ b/src/demo/xray.c
@@ -19,9 +19,9 @@ static const char* leg[] = {
 
 static struct ncplane*
 make_slider(struct notcurses* nc, int dimx){
-  // 862 frames in the video
   const int len = strlen(leg[0]);
-  const int REPS = 862 / len + dimx / len;
+  // 862 frames in the video
+  const int REPS = 862 / len + dimx / len + 1;
   struct ncplane_options nopts = {
     .y = 1,
     .x = 0,
diff --git a/src/fetch/main.c b/src/fetch/main.c
index 394723d0da..b0302089c2 100644
--- a/src/fetch/main.c
+++ b/src/fetch/main.c
@@ -4,6 +4,7 @@
 #include <string.h>
 #include <unistd.h>
 #include <locale.h>
+#include <assert.h>
 #include <strings.h>
 #include <pthread.h>
 #include <semaphore.h>
@@ -22,8 +23,28 @@
 #endif
 #include <notcurses/notcurses.h>
 #include "compat/compat.h"
+#include "builddef.h"
 #include "ncart.h"
 
+static inline const char*
+path_separator(void){
+#ifdef __MINGW64__
+  return "\\";
+#else
+  return "/";
+#endif
+}
+
+static inline char*
+find_data(const char* datum){
+  const char* datadir = NOTCURSES_SHARE;
+  char* path = malloc(strlen(datadir) + 1 + strlen(datum) + 1);
+  strcpy(path, datadir);
+  strcat(path, path_separator());
+  strcat(path, datum);
+  return path;
+}
+
 typedef struct distro_info {
   const char* name;            // must match 'lsb_release -i'
   const char* logofile;        // kept at original aspect ratio, lain atop bg
@@ -392,20 +413,20 @@ get_kernel(fetched_info* fi){
 // windows distro_pretty gets handled in get_kernel() above
 static const distro_info*
 windows_ncneofetch(fetched_info* fi){
-  static const distro_info mswin = {
+  static distro_info mswin = {
     .name = "Windows",
-    .logofile = NULL, // FIXME
   };
+  mswin.logofile = find_data("Windows10Logo.png"),
   fi->neologo = get_neofetch_art("Windows");
   return &mswin;
 }
 
 static const distro_info*
 freebsd_ncneofetch(fetched_info* fi){
-  static const distro_info fbsd = {
+  static distro_info fbsd = {
     .name = "FreeBSD",
-    .logofile = NULL, // FIXME
   };
+  fbsd.logofile = find_data("freebsd.png"),
   fi->neologo = get_neofetch_art("BSD"); // use big daemon logo
   fi->distro_pretty = NULL;
   return &fbsd;
@@ -475,14 +496,13 @@ drawpalette(struct notcurses* nc){
 }
 
 static int
-infoplane_notcurses(struct notcurses* nc, const fetched_info* fi, int planeheight){
+infoplane_notcurses(struct notcurses* nc, const fetched_info* fi,
+                    int planeheight, int nextline){
   const int planewidth = 72;
   unsigned dimy;
-  unsigned y;
   struct ncplane* std = notcurses_stddim_yx(nc, &dimy, NULL);
-  ncplane_cursor_yx(std, &y, NULL);
   struct ncplane_options nopts = {
-    .y = y,
+    .y = nextline,
     .x = NCALIGN_CENTER,
     .rows = planeheight,
     .cols = planewidth,
@@ -573,6 +593,16 @@ infoplane_notcurses(struct notcurses* nc, const fetched_info* fi, int planeheigh
   ncchannels_set_bg_rgb8(&channels, 0x50, 0x50, 0x50);
   ncplane_set_base(infop, " ", 0, channels);
   ncplane_scrollup_child(std, infop);
+  int parend = ncplane_abs_y(std) + ncplane_dim_y(std) - 1; // where parent ends
+  int chend = ncplane_abs_y(infop) + ncplane_dim_y(infop) - 1; // where child ends
+  if(chend > parend){
+    ncplane_move_rel(infop, -(chend - parend), 0);
+  }
+  if(ncplane_putchar_yx(std, ncplane_abs_y(infop) + ncplane_dim_y(infop) - 1,
+                        ncplane_abs_x(infop) + ncplane_dim_x(infop) + 1,
+                        '\n') != 1){
+    return -1;
+  }
   if(notcurses_render(nc)){
     return -1;
   }
@@ -580,14 +610,15 @@ infoplane_notcurses(struct notcurses* nc, const fetched_info* fi, int planeheigh
 }
 
 static int
-infoplane(struct notcurses* nc, const fetched_info* fi){
+infoplane(struct notcurses* nc, const fetched_info* fi, int nextline){
   const int planeheight = 7;
-  int r = infoplane_notcurses(nc, fi, planeheight);
+  int r = infoplane_notcurses(nc, fi, planeheight, nextline);
   r |= notcurses_stop(nc);
   return r;
 }
 
 struct marshal {
+  int nextline; // line following display plane, where info plane ought be placed
   struct notcurses* nc;
   const distro_info* dinfo;
   const char* logo; // read from /etc/os-release (or builtin), may be NULL
@@ -639,9 +670,9 @@ neologo_present(struct notcurses* nc, const char* nlogo){
   ncplane_set_fg_default(n);
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_ITALIC);
   if(notcurses_canopen_images(nc)){
-    ncplane_putstr_aligned(n, -1, NCALIGN_CENTER, "(no image file is known for your distro)");
+    ncplane_putstr_aligned(n, -1, NCALIGN_CENTER, "(no image file is known for your distro)\n");
   }else{
-    ncplane_putstr_aligned(n, -1, NCALIGN_CENTER, "(notcurses was compiled without image support)");
+    ncplane_putstr_aligned(n, -1, NCALIGN_CENTER, "(notcurses was compiled without image support)\n");
   }
   ncplane_off_styles(n, NCSTYLE_BOLD | NCSTYLE_ITALIC);
   return notcurses_render(nc);
@@ -686,8 +717,7 @@ display_thread(void* vmarshal){
         ncplane_move_yx(iplane, y, x);
         ncplane_scrollup_child(notcurses_stdplane(m->nc), iplane);
         notcurses_render(m->nc);
-        ncplane_cursor_move_yx(notcurses_stdplane(m->nc),
-                               ncplane_abs_y(iplane) + ncplane_dim_y(iplane), 0);
+        m->nextline = ncplane_abs_y(iplane) + ncplane_dim_y(iplane);
         return NULL;
       }
     }
@@ -697,6 +727,9 @@ display_thread(void* vmarshal){
       return NULL;
     }
   }
+  unsigned nl;
+  ncplane_cursor_yx(notcurses_stdplane(m->nc), &nl, NULL);
+  m->nextline = nl;
   return NULL;
 }
 
@@ -730,6 +763,7 @@ ncneofetch(struct notcurses* nc){
     .dinfo = fi.distro,
     .logo = fi.logo,
     .neologo = fi.neologo,
+    .nextline = -1,
   };
   pthread_t tid;
   const bool launched = !pthread_create(&tid, NULL, display_thread, &display_marshal);
@@ -746,7 +780,8 @@ ncneofetch(struct notcurses* nc){
   if(launched){
     pthread_join(tid, NULL);
   }
-  if(infoplane(nc, &fi)){
+  assert(display_marshal.nextline >= 0);
+  if(infoplane(nc, &fi, display_marshal.nextline)){
     free_fetched_info(&fi);
     return -1;
   }
diff --git a/src/info/main.c b/src/info/main.c
index 91ce6186f1..36fe132498 100644
--- a/src/info/main.c
+++ b/src/info/main.c
@@ -153,6 +153,9 @@ emoji_viz(struct ncplane* n){
   while(x++ < 80){
     ncplane_putchar(n, ' ');
   }
+  if(ncplane_dim_x(n) > 80){
+    ncplane_putchar(n, '\n');
+  }
   return 0;
 }
 
@@ -327,8 +330,7 @@ unicodedumper(struct ncplane* n, const char* indent){
     uint64_t lr = NCCHANNELS_INITIALIZER(0xff, 0xff, 0xff, 0xdB, 0x18, 0x8E);
     uint64_t ul = NCCHANNELS_INITIALIZER(0xff, 0xff, 0xff, 0x19, 0x19, 0x70);
     uint64_t ll = NCCHANNELS_INITIALIZER(0xff, 0xff, 0xff, 0x19, 0x19, 0x70);
-    ncplane_cursor_move_yx(n, y - 15, 0);
-    ncplane_stain(n, -1, -1, 15, 80, ul, ur, ll, lr);
+    ncplane_stain(n, y - 16, 0, 16, 80, ul, ur, ll, lr);
     ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_ITALIC);
     ncplane_cursor_move_yx(n, y - 12, 54);
     wviz(n, L"🯁🯂🯃https://notcurses.com");
diff --git a/src/lib/blitset.h b/src/lib/blitset.h
index eb564c9cc7..037faba9ce 100644
--- a/src/lib/blitset.h
+++ b/src/lib/blitset.h
@@ -25,7 +25,7 @@ encoding_x_scale(const tinfo* tcache, const struct blitset* bset) {
 
 // Expand NCBLIT_DEFAULT for media blitting, based on environment. We never
 // use NCBLIT_PIXEL for NCBLIT_DEFAULT, though maybe this ought change.
-static inline ncblitter_e 
+static inline ncblitter_e
 rgba_blitter_default(const tinfo* tcache, ncscale_e scale){
   if(!tcache->caps.utf8){
     return NCBLIT_1x1; // only one that works in ASCII
diff --git a/src/lib/direct.c b/src/lib/direct.c
index b86d85f6dc..71ec339cbf 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -829,9 +829,11 @@ ncdirect_stop_minimal(void* vnc){
     ret |= fbuf_finalize(&f, stdout);
   }
   if(nc->tcache.ttyfd >= 0){
-    if(nc->tcache.kbdlevel){
-      if(tty_emit(KKEYBOARD_POP, nc->tcache.ttyfd)){
-        ret = -1;
+    if(!(nc->flags & NCDIRECT_OPTION_DRAIN_INPUT)){
+      if(nc->tcache.kbdlevel){
+        if(tty_emit(KKEYBOARD_POP, nc->tcache.ttyfd)){
+          ret = -1;
+        }
       }
     }
     const char* cnorm = get_escape(&nc->tcache, ESCAPE_CNORM);
diff --git a/src/lib/egcpool.h b/src/lib/egcpool.h
index 9589a2677e..ee5814a0d9 100644
--- a/src/lib/egcpool.h
+++ b/src/lib/egcpool.h
@@ -122,6 +122,7 @@ utf8_egc_len(const char* gcluster, int* colcount){
       if(cols < 0){
         injoin = false;
         if(iswspace(wc)){ // newline or tab
+          *colcount = 1;
           return ret + 1;
         }
         cols = 1;
diff --git a/src/lib/fade.c b/src/lib/fade.c
index 9b0e5e83e1..9e37cf96f0 100644
--- a/src/lib/fade.c
+++ b/src/lib/fade.c
@@ -230,7 +230,7 @@ int ncplane_fadeout_iteration(ncplane* n, ncfadectx* nctx, int iter,
   return ret;
 }
 
-static ncfadectx* 
+static ncfadectx*
 ncfadectx_setup_internal(ncplane* n, const struct timespec* ts){
   if(!ncplane_notcurses(n)->tcache.caps.rgb &&
      !ncplane_notcurses(n)->tcache.caps.can_change_colors){ // terminal can't fade
diff --git a/src/lib/in.c b/src/lib/in.c
index d2d0ce6272..1561390b17 100644
--- a/src/lib/in.c
+++ b/src/lib/in.c
@@ -21,7 +21,7 @@
 // without unbounded allocation, and also without losing data. We'd furthermore
 // like to reliably differentiate escapes and regular input, even when that
 // latter contains escapes. Unbounded input will hopefully only be present when
-// redirected from a file (NCOPTION_TOSS_INPUT)
+// redirected from a file.
 
 static sig_atomic_t cont_seen;
 static sig_atomic_t resize_seen;
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 02c7d3226f..bbcd7223de 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -1197,10 +1197,6 @@ goto_location(notcurses* nc, fbuf* f, int y, int x, const ncplane* srcp){
       return -1;
     }
   }
-  if(y > nc->rstate.logendy || (y == nc->rstate.logendy && x > nc->rstate.logendx)){
-    nc->rstate.logendy = y;
-    nc->rstate.logendx = x;
-  }
   nc->rstate.x = x;
   nc->rstate.y = y;
   nc->rstate.lastsrcp = srcp;
@@ -1389,7 +1385,10 @@ pool_blit_direct(egcpool* pool, nccell* c, const char* gcluster, int bytes, int
   if(bytes < 0 || cols < 0){
     return -1;
   }
-  if(is_control_egc((const unsigned char*)gcluster, bytes)){
+  // we allow newlines to be blitted into the pool, as they're picked up and
+  // given special semantics by paint() and rasterization.
+  if(*gcluster != '\n' && is_control_egc((const unsigned char*)gcluster, bytes)){
+    logerror("not loading control character %u\n", *(const unsigned char*)gcluster);
     return -1;
   }
   c->width = cols;
@@ -1555,7 +1554,7 @@ rgba_trans_p(uint32_t p, uint32_t transcolor){
   if(ncpixel_a(p) < 192){
     return true;
   }
-  if(transcolor && 
+  if(transcolor &&
       (ncpixel_r(p) == (transcolor & 0xff0000ull) >> 16) &&
       (ncpixel_g(p) == (transcolor & 0xff00ull) >> 8) &&
       (ncpixel_b(p) == (transcolor & 0xffull))){
@@ -1811,6 +1810,7 @@ cancel_and_join(const char* name, pthread_t tid, void** res){
 
 static inline int
 emit_scrolls(const tinfo* ti, int count, fbuf* f){
+  logdebug("emitting %d scrolls\n", count);
   if(count > 1){
     const char* indn = get_escape(ti, ESCAPE_INDN);
     if(indn){
@@ -1833,6 +1833,18 @@ emit_scrolls(const tinfo* ti, int count, fbuf* f){
   return 0;
 }
 
+// both emit the |count > 0| scroll ops to |f|, and update the cursor
+// tracking in |nc|
+static inline int
+emit_scrolls_track(notcurses* nc, int count, fbuf* f){
+  if(emit_scrolls(&nc->tcache, count, f)){
+    return -1;
+  }
+  nc->rstate.y -= count;
+  nc->rstate.x = 0;
+  return 0;
+}
+
 // replace or populate the TERM environment variable with 'termname'
 int putenv_term(const char* termname) __attribute__ ((nonnull (1)));
 
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index 90e2ac3ac6..6650b66227 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -660,7 +660,7 @@ deflate_buf(void* buf, fbuf* f, int dimy, int dimx){
 }
 
 // copy |encodeable| ([1..3]) pixels from |src| to the buffer |dst|, setting
-// alpha along the way according to |wipe|. 
+// alpha along the way according to |wipe|.
 static inline int
 add_to_buf(uint32_t *dst, const uint32_t* src, int encodeable, bool wipe[static 3]){
   dst[0] = *src++;
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 4c61796b81..388a15ef7a 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -101,14 +101,17 @@ notcurses_stop_minimal(void* vnc){
     }
     // don't use use leave_alternate_screen() here; we need pop the keyboard
     // whether we're in regular or alternate screen, and we need it done
-    // before returning to the regular screen if we're in the alternate.
-    if(nc->tcache.kbdlevel){
-      if(tty_emit(KKEYBOARD_POP, nc->tcache.ttyfd)){
-        ret = -1;
-      }
-    }else{
-      if(tty_emit(XTMODKEYSUNDO, nc->tcache.ttyfd)){
-        ret = -1;
+    // before returning to the regular screen if we're in the alternate. if
+    // we drained input, we never sent a keyboard modifier; send none now.
+    if(!(nc->flags & NCOPTION_DRAIN_INPUT)){
+      if(nc->tcache.kbdlevel){
+        if(tty_emit(KKEYBOARD_POP, nc->tcache.ttyfd)){
+          ret = -1;
+        }
+      }else{
+        if(tty_emit(XTMODKEYSUNDO, nc->tcache.ttyfd)){
+          ret = -1;
+        }
       }
     }
     if(nc->tcache.in_alt_screen){
@@ -1279,16 +1282,12 @@ int notcurses_stop(notcurses* nc){
   if(nc){
     ret |= notcurses_stop_minimal(nc);
     // if we were not using the alternate screen, our cursor's wherever we last
-    // wrote. move it to the bottom left of the screen, *unless*
-    // PRESERVE_CURSOR was used, which is a bit more complex.
-    if((nc->flags & NCOPTION_PRESERVE_CURSOR) || !get_escape(&nc->tcache, ESCAPE_SMCUP)){
-      unsigned targy = nc->rstate.logendy;
+    // wrote. move it to the furthest place to which it advanced.
+    if(!get_escape(&nc->tcache, ESCAPE_SMCUP)){
       fbuf_reset(&nc->rstate.f);
-      if(++targy >= nc->lfdimy){
-        fbuf_putc(&nc->rstate.f, '\n');
-        --targy;
-      }
-      goto_location(nc, &nc->rstate.f, targy, 0, NULL);
+//fprintf(stderr, "CLOSING TO %d/%d\n", nc->rstate.logendy, nc->rstate.logendx);
+      goto_location(nc, &nc->rstate.f, nc->rstate.logendy, nc->rstate.logendx, NULL);
+//fprintf(stderr, "***"); fflush(stderr);
       fbuf_finalize(&nc->rstate.f, stdout);
     }
     if(nc->stdplane){
@@ -1604,15 +1603,15 @@ void scroll_down(ncplane* n){
       nccell_release(n, &row[clearx]);
     }
     memset(row, 0, sizeof(*row) * n->lenx);
-  }else{
-    ++n->y;
-  }
-  for(struct ncplane* c = n->blist ; c ; c = c->bnext){
-    if(!c->fixedbound){
-      if(ncplanes_intersect_p(n, c)){
-        ncplane_move_rel(c, -1, 0);
+    for(struct ncplane* c = n->blist ; c ; c = c->bnext){
+      if(!c->fixedbound){
+        if(ncplanes_intersect_p(n, c)){
+          ncplane_move_rel(c, -1, 0);
+        }
       }
     }
+  }else{
+    ++n->y;
   }
 }
 
@@ -1643,8 +1642,8 @@ int ncplane_scrollup_child(ncplane* n, const ncplane* child){
     logerror("child plane is fixed\n");
     return -1;
   }
-  int parend = ncplane_abs_y(n) + ncplane_dim_y(n); // where parent ends
-  int chend = ncplane_abs_y(child) + ncplane_dim_y(child); // where child ends
+  int parend = ncplane_abs_y(n) + ncplane_dim_y(n) - 1; // where parent ends
+  int chend = ncplane_abs_y(child) + ncplane_dim_y(child) - 1; // where child ends
   if(chend <= parend){
     return 0;
   }
@@ -1668,7 +1667,7 @@ int cell_load(ncplane* n, nccell* c, const char* gcluster){
 // either or both of |y|/|x| is -1, the current cursor location for that
 // dimension will be used. if the glyph cannot fit on the current line, it is
 // an error unless scrolling is enabled.
-static inline int
+static int
 ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
             uint16_t stylemask, uint64_t channels, int bytes){
   if(n->sprite){
@@ -1707,7 +1706,6 @@ ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
   }
   if(*egc == '\n'){
     scroll_down(n);
-    return 0;
   }
   // A wide character obliterates anything to its immediate right (and marks
   // that cell as wide). Any character placed atop one cell of a wide character
@@ -1737,18 +1735,20 @@ ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
   }
 //fprintf(stderr, "%08x %016lx %c %d %d\n", targ->gcluster, targ->channels, nccell_double_wide_p(targ) ? 'D' : 'd', bytes, cols);
   // must set our right hand sides wide, and check for further damage
-  ++n->x;
-  for(int i = 1 ; i < cols ; ++i){
-    nccell* candidate = &n->fb[nfbcellidx(n, n->y, n->x)];
-    int off = nccell_cols(candidate);
-    nccell_release(n, &n->fb[nfbcellidx(n, n->y, n->x)]);
-    while(--off > 0){
-      nccell_obliterate(n, &n->fb[nfbcellidx(n, n->y, n->x + off)]);
-    }
-    candidate->channels = targ->channels;
-    candidate->stylemask = targ->stylemask;
-    candidate->width = targ->width;
+  if(*egc != '\n'){
     ++n->x;
+    for(int i = 1 ; i < cols ; ++i){
+      nccell* candidate = &n->fb[nfbcellidx(n, n->y, n->x)];
+      int off = nccell_cols(candidate);
+      nccell_release(n, &n->fb[nfbcellidx(n, n->y, n->x)]);
+      while(--off > 0){
+        nccell_obliterate(n, &n->fb[nfbcellidx(n, n->y, n->x + off)]);
+      }
+      candidate->channels = targ->channels;
+      candidate->stylemask = targ->stylemask;
+      candidate->width = targ->width;
+      ++n->x;
+    }
   }
   return cols;
 }
diff --git a/src/lib/reel.c b/src/lib/reel.c
index 034d22aab1..27f27102bc 100644
--- a/src/lib/reel.c
+++ b/src/lib/reel.c
@@ -390,7 +390,7 @@ ncreel_draw_tablet(const ncreel* nr, nctablet* t, int frontiertop,
       }
       // We needn't move the resized plane if drawing down, or the focused plane.
       // The focused tablet will have been resized properly above, but it might
-      // be out of position (the focused tablet ought move as little as possible). 
+      // be out of position (the focused tablet ought move as little as possible).
       // Move it back to the frontier, or the nearest line above if it has grown.
       if(nr->tablets == t){
         if(leny - frontiertop + 1 < ll){
diff --git a/src/lib/render.c b/src/lib/render.c
index 52bdb3ccfe..0c84c86994 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -227,8 +227,7 @@ paint_sprixel(ncplane* p, struct crender* rvec, int starty, int startx,
 // the sprixelstack orders sprixels of the plane (so we needn't keep them
 // ordered between renders). each time we meet a sprixel, extract it from
 // the pile's sprixel list, and update the sprixelstack.
-__attribute__ ((nonnull (1, 2, 7)))
-static void
+__attribute__ ((nonnull (1, 2, 7))) static void
 paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
       int dstabsy, int dstabsx, sprixel** sprixelstack){
   unsigned y, x, dimy, dimx;
@@ -289,6 +288,12 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
       if(nccell_wide_right_p(targc)){
         continue;
       }
+      // if we encounter a newline (always a bare linefeed; Windows CRLF
+      // translation only matters for text stdio), we're done with this
+      // line *for this plane*.
+      if(targc->gcluster == '\n'){
+        break;
+      }
 
       if(nccell_fg_alpha(targc) > NCALPHA_OPAQUE){
         const nccell* vis = &p->fb[nfbcellidx(p, y, x)];
@@ -621,8 +626,9 @@ static int
 term_putc(fbuf* f, const egcpool* e, const nccell* c){
   if(cell_simple_p(c)){
 //fprintf(stderr, "[%.4s] %08x\n", (const char*)&c->gcluster, c->gcluster); }
-    // we must not have any 'cntrl' characters at this point
-    if(c->gcluster == 0){
+    // we must not have any 'cntrl' characters at this point, except for
+    // nil or newline
+    if(c->gcluster == 0 || c->gcluster == '\n'){
       if(fbuf_putc(f, ' ') < 0){
         return -1;
       }
@@ -991,13 +997,13 @@ rasterize_scrolls(const ncpile* p, fbuf* f){
     return 0;
   }
   logdebug("order-%d scroll\n", scrolls);
-  if(p->nc->rstate.logendy >= 0){
+  /*if(p->nc->rstate.logendy >= 0){
     p->nc->rstate.logendy -= scrolls;
     if(p->nc->rstate.logendy < 0){
       p->nc->rstate.logendy = 0;
       p->nc->rstate.logendx = 0;
     }
-  }
+  }*/
   // FIXME probably need this to take place at the end of cycle...
   if(p->nc->tcache.pixel_scroll){
     p->nc->tcache.pixel_scroll(p, &p->nc->tcache, scrolls);
@@ -1012,7 +1018,7 @@ rasterize_scrolls(const ncpile* p, fbuf* f){
       return -1;
     }
   }
-  return emit_scrolls(&p->nc->tcache, scrolls, f);
+  return emit_scrolls_track(p->nc, scrolls, f);
 }
 
 // second sprixel pass in rasterization. by this time, all sixels are handled
@@ -1117,6 +1123,7 @@ rasterize_core(notcurses* nc, const ncpile* p, fbuf* f, unsigned phase){
   // bit per coordinate, one per struct crender.
   for(unsigned y = nc->margin_t; y < p->dimy + nc->margin_t ; ++y){
     const int innery = y - nc->margin_t;
+    bool saw_linefeed = 0;
     for(unsigned x = nc->margin_l ; x < p->dimx + nc->margin_l ; ++x){
       const int innerx = x - nc->margin_l;
       const size_t damageidx = innery * nc->lfdimx + innerx;
@@ -1217,6 +1224,9 @@ rasterize_core(notcurses* nc, const ncpile* p, fbuf* f, unsigned phase){
         if(term_putc(f, &nc->pool, srccell)){
           return -1;
         }
+        if(srccell->gcluster == '\n'){
+          saw_linefeed = true;
+        }
         rvec[damageidx].s.damaged = 0;
         rvec[damageidx].s.p_beats_sprixel = 0;
         nc->rstate.x += srccell->width;
@@ -1225,9 +1235,31 @@ rasterize_core(notcurses* nc, const ncpile* p, fbuf* f, unsigned phase){
         }else{
           ++nc->rstate.x;
         }
+        if((int)y > nc->rstate.logendy || ((int)y == nc->rstate.logendy && (int)x > nc->rstate.logendx)){
+          if((int)y > nc->rstate.logendy){
+//fprintf(stderr, "**************8NATURAL PLACEMENT AT %u/ %u\n", y, x);
+            nc->rstate.logendy = y;
+            nc->rstate.logendx = 0;
+          }
+          if(x >= p->dimx + nc->margin_l - 1){
+            if(nc->rstate.logendy < (int)(p->dimy + nc->margin_t - 1)){
+              ++nc->rstate.logendy;
+            }
+            nc->rstate.logendx = 0;
+            saw_linefeed = false;
+//fprintf(stderr, "**********8SCROLLING PLACEMENT AT %u %u\n", nc->rstate.logendy, nc->rstate.logendx);
+          }else if((int)x >= nc->rstate.logendx){
+            nc->rstate.logendx = x;
+//fprintf(stderr, "**********HORIZ PLACEMENT AT %u %u\n", nc->rstate.logendy, nc->rstate.logendx);
+          }
+        }
       }
 //fprintf(stderr, "damageidx: %ld\n", damageidx);
     }
+    // shouldn't this happen only if the linefeed was on the last line? FIXME
+    if(saw_linefeed){
+      nc->rstate.logendx = 0;
+    }
   }
   return 0;
 }
@@ -1366,19 +1398,16 @@ int clear_and_home(notcurses* nc, tinfo* ti, fbuf* f){
   const char* clearscr = get_escape(ti, ESCAPE_CLEAR);
   if(clearscr){
     if(fbuf_emit(f, clearscr) == 0){
-      goto success;
+      nc->rstate.x = nc->rstate.y = 0;
+      return 0;
     }
   }
-  if(emit_scrolls(ti, ncplane_dim_y(notcurses_stdplane_const(nc)), f)){
+  if(emit_scrolls_track(nc, ncplane_dim_y(notcurses_stdplane_const(nc)), f)){
     return -1;
   }
   if(goto_location(nc, f, 0, 0, NULL)){
     return -1;
   }
-
-success:
-  nc->rstate.x = 0;
-  nc->rstate.y = 0;
   return 0;
 }
 
diff --git a/src/lib/sprite.c b/src/lib/sprite.c
index 93f67c6bfb..52d46db718 100644
--- a/src/lib/sprite.c
+++ b/src/lib/sprite.c
@@ -219,8 +219,14 @@ int sprite_clear_all(const tinfo* t, fbuf* f){
   return t->pixel_clear_all(f);
 }
 
+// we don't want to seed the process-wide prng, but we do want to stir in a
+// bit of randomness for our purposes. we probably ought just use platform-
+// specific APIs, but for now, throw the timestamp in there, lame FIXME.
 int sprite_init(const tinfo* t, int fd){
-  sprixelid_nonce = rand() % 0xffffffu;
+  struct timeval tv;
+  gettimeofday(&tv, NULL);
+  int stir = (tv.tv_sec >> 3) ^ tv.tv_usec;
+  sprixelid_nonce = (rand() ^ stir) % 0xffffffu;
   if(t->pixel_init == NULL){
     return 0;
   }
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index fc75ef1cdf..e05df83346 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -416,21 +416,39 @@ init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
 // and geometry. send XTGETTCAP for terminal name. if 'minimal' is set, don't
 // send any identification queries (we've already identified the terminal).
 // write DSRCPR as early as possible, so that it precedes any query material
-// that's bled onto stdin and echoed.
+// that's bled onto stdin and echoed. if 'noaltscreen' is set, do not send
+// an smcup. if 'draininput' is set, do not send any keyboard modifiers.
 static int
-send_initial_queries(int fd, bool minimal, bool noaltscreen){
+send_initial_queries(int fd, unsigned minimal, unsigned noaltscreen,
+                     unsigned draininput){
   const char *queries;
   if(noaltscreen){
     if(minimal){
-      queries = DSRCPR KKBDENTER DIRECTIVES;
+      if(draininput){
+        queries = DSRCPR DIRECTIVES;
+      }else{
+        queries = DSRCPR KKBDENTER DIRECTIVES;
+      }
     }else{
-      queries = DSRCPR KKBDENTER IDQUERIES DIRECTIVES;
+      if(draininput){
+        queries = DSRCPR IDQUERIES DIRECTIVES;
+      }else{
+        queries = DSRCPR KKBDENTER IDQUERIES DIRECTIVES;
+      }
     }
   }else{
     if(minimal){
-      queries = SMCUP DSRCPR KKBDENTER DIRECTIVES;
+      if(draininput){
+        queries = SMCUP DSRCPR DIRECTIVES;
+      }else{
+        queries = SMCUP DSRCPR KKBDENTER DIRECTIVES;
+      }
     }else{
-      queries = SMCUP DSRCPR KKBDENTER IDQUERIES DIRECTIVES;
+      if(draininput){
+        queries = SMCUP DSRCPR IDQUERIES DIRECTIVES;
+      }else{
+        queries = SMCUP DSRCPR KKBDENTER IDQUERIES DIRECTIVES;
+      }
     }
   }
   size_t len = strlen(queries);
@@ -453,6 +471,7 @@ int enter_alternate_screen(FILE* fp, tinfo* ti, bool flush){
   if(term_emit(smcup, fp, false) < 0){
     return -1;
   }
+  // probably don't want to send these if we've drained input...FIXME
   if(ti->kbdlevel){
     if(term_emit(KKBDENTER, fp, flush)){
       return -1;
@@ -475,6 +494,7 @@ int leave_alternate_screen(FILE* fp, tinfo* ti){
     logerror("can't leave alternate screen");
     return -1;
   }
+  // probably don't want to send these if we've drained input...FIXME
   if(ti->kbdlevel){
     if(term_emit(KKEYBOARD_POP, fp, false) ||
       term_emit(rmcup, fp, false) ||
@@ -867,7 +887,7 @@ int interrogate_terminfo(tinfo* ti, FILE* out, unsigned utf8,
     // if we already know our terminal (e.g. on the linux console), there's no
     // need to send the identification queries. the controls are sufficient.
     bool minimal = (ti->qterm != TERMINAL_UNKNOWN);
-    if(send_initial_queries(ti->ttyfd, minimal, noaltscreen)){
+    if(send_initial_queries(ti->ttyfd, minimal, noaltscreen, draininput)){
       goto err;
     }
   }
diff --git a/src/lib/windows.c b/src/lib/windows.c
index 6ddb16e52e..8771d83662 100644
--- a/src/lib/windows.c
+++ b/src/lib/windows.c
@@ -36,7 +36,7 @@ int prepare_windows_terminal(tinfo* ti, size_t* tablelen, size_t* tableused){
     { ESCAPE_RMUL,  "\x1b[24m", },
     { ESCAPE_SGR0,  "\x1b[0m", },
     { ESCAPE_MAX, NULL, }
-  }, *w; 
+  }, *w;
   for(w = wterms ; w->tinfo; ++w){
     if(grow_esc_table(ti, w->tinfo, w->esc, tablelen, tableused)){
       return -1;
diff --git a/src/poc/cli.c b/src/poc/cli.c
index 0bad1eef73..851df3eb7f 100644
--- a/src/poc/cli.c
+++ b/src/poc/cli.c
@@ -11,12 +11,24 @@ int main(void){
     return EXIT_FAILURE;
   }
   struct ncplane* stdn = notcurses_stdplane(nc);
-  ncplane_putstr(stdn, "press any key");
-  notcurses_render(nc);
+  ncplane_set_scrolling(stdn, true);
+  if(ncplane_putstr(stdn, "press any key\n") < 0){
+    goto err;
+  }
+  if(notcurses_render(nc)){
+    goto err;
+  }
   ncinput ni;
   do{
     notcurses_get_blocking(nc, &ni);
   }while(ni.evtype == NCTYPE_RELEASE);
+  if(notcurses_render(nc)){
+    goto err;
+  }
   notcurses_stop(nc);
   return EXIT_SUCCESS;
+
+err:
+  notcurses_stop(nc);
+  return EXIT_FAILURE;
 }
diff --git a/src/poc/cli2.c b/src/poc/cli2.c
index ee4681c741..ab0ced2754 100644
--- a/src/poc/cli2.c
+++ b/src/poc/cli2.c
@@ -1,17 +1,17 @@
 #include <notcurses/notcurses.h>
 
-int main(void){                                                                                                                     
-  struct notcurses_options opts = {                                                                                                 
-    .flags = NCOPTION_NO_ALTERNATE_SCREEN |                                                                                         
-             NCOPTION_PRESERVE_CURSOR |                                                                                             
-             NCOPTION_NO_CLEAR_BITMAPS |                                                                                            
-             NCOPTION_DRAIN_INPUT,                                                                                                  
-  };                                                                                                                                
-  struct notcurses* nc = notcurses_init(&opts, NULL);                                                                               
-  if(nc == NULL){                                                                                                                   
-    return EXIT_FAILURE;                                                                                                            
-  }                                                                                                                                 
+int main(void){
+  struct notcurses_options opts = {
+    .flags = NCOPTION_NO_ALTERNATE_SCREEN |
+             NCOPTION_PRESERVE_CURSOR |
+             NCOPTION_NO_CLEAR_BITMAPS |
+             NCOPTION_DRAIN_INPUT,
+  };
+  struct notcurses* nc = notcurses_init(&opts, NULL);
+  if(nc == NULL){
+    return EXIT_FAILURE;
+  }
   notcurses_render(nc);
-  notcurses_stop(nc);                                                                                                               
-  return EXIT_SUCCESS;                                                                                                              
+  notcurses_stop(nc);
+  return EXIT_SUCCESS;
 }
diff --git a/src/poc/sgr-full.c b/src/poc/sgr-full.c
index 5ff0903a47..63fb47624c 100644
--- a/src/poc/sgr-full.c
+++ b/src/poc/sgr-full.c
@@ -67,8 +67,8 @@ int main(void){
   ncplane_putstr(n, "a ═ bold underline italic struck\n");
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_UNDERCURL | NCSTYLE_ITALIC | NCSTYLE_STRUCK);
   ncplane_putstr(n, "a ═ bold undercurl italic struck\n");
-
   ncplane_set_styles(n, NCSTYLE_NONE);
+  ncplane_putstr(n, "a ═ none\n");
   if(notcurses_render(nc)){
     goto err;
   }
diff --git a/src/tetris/background.h b/src/tetris/background.h
index ac973b46c0..83091ad304 100644
--- a/src/tetris/background.h
+++ b/src/tetris/background.h
@@ -32,7 +32,9 @@ void DrawBoard() { // draw all fixed components of the game
   scoreplane_->set_base("", 0, scorechan);
   scoreplane_->set_bg_alpha(NCALPHA_TRANSPARENT);
   scoreplane_->set_fg_rgb(0xd040d0);
-  scoreplane_->printf(0, 1, "%s", *std::make_unique<char*>(notcurses_accountname()));
+  char* n = notcurses_accountname();
+  scoreplane_->printf(0, 1, "%s", n);
+  free(n);
   scoreplane_->set_fg_rgb(0x00d0a0);
   UpdateScore();
   nc_.render();


Test patch:
diff --git a/src/tests/cell.cpp b/src/tests/cell.cpp
index 77248c200a..d890b3a8ce 100644
--- a/src/tests/cell.cpp
+++ b/src/tests/cell.cpp
@@ -356,13 +356,13 @@ TEST_CASE("Cell") {
     CHECK(0 == strcmp(nccell_extended_gcluster(n_, &c), "*"));
   }
 
-  // only space is allowed
+  // only space/newline is allowed from control char whitespace
   SUBCASE("CellLoadCharWhitespace") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
-    CHECK(-1 == nccell_load_char(n_, &c, '\n'));
     CHECK(-1 == nccell_load_char(n_, &c, '\f'));
     CHECK(-1 == nccell_load_char(n_, &c, '\v'));
     CHECK(-1 == nccell_load_char(n_, &c, '\t'));
+    CHECK(1 == nccell_load_char(n_, &c, '\n'));
     CHECK(1 == nccell_load_char(n_, &c, ' '));
   }
 
diff --git a/src/tests/main.cpp b/src/tests/main.cpp
index 59676abd99..a86a28b3ea 100644
--- a/src/tests/main.cpp
+++ b/src/tests/main.cpp
@@ -7,7 +7,6 @@
 #include <iostream>
 #include <climits>
 #include <sys/stat.h>
-#include <filesystem>
 
 const char* datadir = NOTCURSES_SHARE;
 // NCLOGLEVEL_INFO for initial testing framework creation. we then switch to
diff --git a/src/tests/media.cpp b/src/tests/media.cpp
index 58bd55fd27..7500433594 100644
--- a/src/tests/media.cpp
+++ b/src/tests/media.cpp
@@ -3,6 +3,9 @@
 #include <vector>
 #include <cmath>
 
+// only run through this many frames of video
+constexpr auto FRAMECOUNT = 100;
+
 TEST_CASE("Media") {
   auto nc_ = testing_notcurses();
   REQUIRE(nullptr != nc_);
@@ -142,7 +145,8 @@ TEST_CASE("Media") {
       auto ncv = ncvisual_from_file(find_data("notcursesIII.mov").get());
       REQUIRE(ncv);
       // FIXME can't we use use ncvisual_stream() here?
-      for(;;){ // run at the highest speed we can
+      // run at the highest speed we can
+      for(int frame = 0 ; frame < FRAMECOUNT ; ++frame){
         int ret = ncvisual_decode(ncv);
         if(1 == ret){
           break;
@@ -165,7 +169,8 @@ TEST_CASE("Media") {
       ncplane_dim_yx(ncp_, &dimy, &dimx);
       auto ncv = ncvisual_from_file(find_data("notcursesIII.mov").get());
       REQUIRE(ncv);
-      for(;;){ // run at the highest speed we can
+      // run at the highest speed we can
+      for(int frame = 0 ; frame < FRAMECOUNT ; ++frame){
         int ret = ncvisual_decode(ncv);
         if(1 == ret){
           break;
@@ -189,7 +194,8 @@ TEST_CASE("Media") {
       ncplane_dim_yx(ncp_, &dimy, &dimx);
       auto ncv = ncvisual_from_file(find_data("notcursesIII.mov").get());
       REQUIRE(ncv);
-      for(;;){ // run at the highest speed we can
+      // run at the highest speed we can
+      for(int frame = 0 ; frame < FRAMECOUNT ; ++frame){
         int ret = ncvisual_decode(ncv);
         if(1 == ret){
           break;
@@ -211,8 +217,8 @@ TEST_CASE("Media") {
       unsigned dimy, dimx;
       ncplane_dim_yx(ncp_, &dimy, &dimx);
       auto ncv = ncvisual_from_file(find_data("notcursesIII.mov").get());
-      REQUIRE(ncv);
-      for(;;){ // run at the highest speed we can
+      // run at the highest speed we can
+      for(int frame = 0 ; frame < FRAMECOUNT ; ++frame){
         int ret = ncvisual_decode(ncv);
         if(1 == ret){
           break;
@@ -234,8 +240,8 @@ TEST_CASE("Media") {
       unsigned dimy, dimx;
       ncplane_dim_yx(ncp_, &dimy, &dimx);
       auto ncv = ncvisual_from_file(find_data("notcursesIII.mov").get());
-      REQUIRE(ncv);
-      for(;;){ // run at the highest speed we can
+      // run at the highest speed we can
+      for(int frame = 0 ; frame < FRAMECOUNT ; ++frame){
         int ret = ncvisual_decode(ncv);
         if(1 == ret){
           break;
diff --git a/src/tests/scrolling.cpp b/src/tests/scrolling.cpp
index d50912daf1..d19c502693 100644
--- a/src/tests/scrolling.cpp
+++ b/src/tests/scrolling.cpp
@@ -275,7 +275,7 @@ TEST_CASE("Scrolling") {
     CHECK(0 == notcurses_render(nc_));
     for(unsigned i = 0 ; i < ncplane_dim_y(np) + starty ; ++i){
       CHECK(starty - i == ncplane_y(np));
-      CHECK(0 == ncplane_putchar(n_, '\n'));
+      CHECK(1 == ncplane_putchar(n_, '\n'));
       CHECK(0 == notcurses_render(nc_));
     }
     CHECK(0 == ncplane_destroy(np));
@@ -303,7 +303,7 @@ TEST_CASE("Scrolling") {
     CHECK(0 == notcurses_render(nc_));
     for(unsigned i = 0 ; i < ncplane_dim_y(np) + starty ; ++i){
       CHECK(starty == ncplane_y(np));
-      CHECK(0 == ncplane_putchar(n_, '\n'));
+      CHECK(1 == ncplane_putchar(n_, '\n'));
       CHECK(0 == notcurses_render(nc_));
     }
     CHECK(0 == ncplane_destroy(np));