Repository: dankamongmen/notcurses (language: python)

Issue:
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].
support some manner of tabs
NCURSES has soft tabs and all that jazz. Right now I believe we reject any '\t' character, as we do any other C0 control code. We should almost certainly expand it into a series of spaces rather than rejecting it. We're not going to be outputting tab characters anytime soon, but we ought come up with sensible internal conversion rules, and perhaps allow tabstops to be configured as NCURSES does. Update the README.md if we do =].


Discussion / hints:
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.
yeah we need tabs sooner rather than later. all aboard!
so the most fundamental question would be: when the user emits a tab, is it:

* immediately converted into spaces at the `ncplane`,
* one wide EGC at the `ncplane`, converted into spaces at render, or
* one wide EGC at the `ncplane`, emitted as a tab?

the easiest thing for us is the first answer. the maximum user fidelity is had with the last. i don't see any reason why we would go with the middle option.

if we always replace with spaces, many problems are immediately solved involving intersecting planes (these are similar to the general problems of wide EGCs). the situation gets a bit confusing around the number of bytes written vs expected, the number of visual characters in the region vs expected, backspacing, and other small details. but we keep everything virtual -- we never need mess with, nor rely on, real world tab settings.
i think the tabs ought definitely be plane-relative. otherwise, moving a plane would keep its tabbed text at the same place, leading to great madness. i think that settles that question.
i think tabs relative to planes, immediately replaced with spaces, is the only sane setup for our model. let's go ahead and implement it.

Fix patch:
diff --git a/NEWS.md b/NEWS.md
index a5f428e44..92c20828a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,8 @@ This document attempts to list user-visible changes and any major internal
 rearrangements of Notcurses.
 
 * 3.0.7 (not yet released)
+  * Tab characters may now be used with `ncplane_put*()`. See
+     `ncplane_output.3` for more information.
 
 * 3.0.6 (2022-02-09)
   * `ncplane_set_[fb]channel()`, `ncplane_[fb]channel()`,
diff --git a/doc/man/man3/notcurses_output.3.md b/doc/man/man3/notcurses_output.3.md
index 147aa4470..252953784 100644
--- a/doc/man/man3/notcurses_output.3.md
+++ b/doc/man/man3/notcurses_output.3.md
@@ -100,8 +100,12 @@ breaks. For more information, consult [Unicode® Standard Annex #29](https://uni
 Functions accepting a set of EGCs must consist of a series of well-formed EGCs,
 broken by cluster breaks, terminated by the appropriate NUL terminator.
 
-Control characters are rejected, except for a newline when the output plane
-is in scrolling mode. A newline outside of scrolling mode will be rejected.
+Control characters are rejected, aside from two exceptions: a horizontal tab
+(**'\t'**, 0x09), and a newline (**'\n'**, 0x0a) *when the output plane
+is in scrolling mode*. A newline outside of scrolling mode will be rejected.
+A tab will advance to the next tab stop, filling the space between with spaces.
+Tab stops are separated by eight columns, and the first character of each line
+is a tab stop.
 
 These functions output to the **ncplane**'s current cursor location. Aside from
 **ncplane_puttext()**, they *do not* move to the next line upon reaching the
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 78d5241fe..3c91c596d 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -727,6 +727,7 @@ typedef struct nccell {
 // protect against such misuse here. problems *will* ensue. similarly, do not
 // set channel flags other than colors/alpha. we assign non-printing glyphs
 // a width of 1 to match utf8_egc_len()'s behavior for whitespace/NUL.
+// FIXME can we enforce this with static_assert?
 #define NCCELL_INITIALIZER(c, s, chan) { .gcluster = (htole(c)), .gcluster_backstop = 0,\
   .width = (uint8_t)((wcwidth(c) < 0 || !c) ? 1 : wcwidth(c)), .stylemask = (s), .channels = (chan), }
 // python fails on #define CELL_CHAR_INITIALIZER(c) CELL_INITIALIZER(c, 0, 0)
@@ -2164,8 +2165,9 @@ ncplane_putc(struct ncplane* n, const nccell* c){
 }
 
 // Replace the cell at the specified coordinates with the provided 7-bit char
-// 'c'. Advance the cursor by 1. On success, returns 1. On failure, returns -1.
-// This works whether the underlying char is signed or unsigned.
+// 'c'. Advance the cursor by 1. On success, returns the number of columns the
+// cursor was advanced. On failure, returns -1. This works whether the
+// underlying char is signed or unsigned.
 static inline int
 ncplane_putchar_yx(struct ncplane* n, int y, int x, char c){
   nccell ce = NCCELL_INITIALIZER((uint32_t)c, ncplane_styles(n), ncplane_channels(n));
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 6743a2286..fc304fd4d 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -1427,7 +1427,7 @@ plane_blit_sixel(sprixel* spx, fbuf* f, int leny, int lenx,
 static inline bool
 is_control_egc(const unsigned char* egc, int bytes){
   if(bytes == 1){
-    if(*egc && iscntrl(*egc)){
+    if(*egc < 0x20 || *egc == 0x7f){ // includes NUL terminator
       return true;
     }
   }else if(bytes == 2){
@@ -1452,9 +1452,9 @@ pool_blit_direct(egcpool* pool, nccell* c, const char* gcluster, int bytes, int
   if(bytes < 0 || cols < 0){
     return -1;
   }
-  // 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)){
+  // we allow newlines and tabs to be blitted into the pool, as they're picked
+  // up and given special semantics by output, paint(), and rasterization.
+  if(is_control_egc((const unsigned char*)gcluster, bytes) && *gcluster != '\n' && *gcluster != '\t'){
     logerror("not loading control character %u", *(const unsigned char*)gcluster);
     return -1;
   }
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index f93f547e7..de675e0d8 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -19,6 +19,7 @@
 #include "banner.h"
 
 #define ESC "\x1b"
+#define TABSTOP 8
 
 void notcurses_version_components(int* major, int* minor, int* patch, int* tweak){
   *major = NOTCURSES_VERNUM_MAJOR;
@@ -1810,17 +1811,19 @@ ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
     return -1;
   }
   // reject any control character for output other than newline (and then only
-  // on a scrolling plane).
-  if(*egc == '\n'){
-    // if we're not scrolling, autogrow would be to the right (as opposed to
-    // down), and thus it still wouldn't apply to the case of a newline.
-    if(!n->scrolling){
-      logerror("rejecting newline on non-scrolling plane");
+  // on a scrolling plane) and tab.
+  if(is_control_egc((const unsigned char*)egc, bytes)){
+    if(*egc == '\n'){
+      // if we're not scrolling, autogrow would be to the right (as opposed to
+      // down), and thus it still wouldn't apply to the case of a newline.
+      if(!n->scrolling){
+        logerror("rejecting newline on non-scrolling plane");
+        return -1;
+      }
+    }else if(*egc != '\t'){
+      logerror("rejecting %dB control character", bytes);
       return -1;
     }
-  }else if(is_control_egc((const unsigned char*)egc, bytes)){
-    logerror("rejecting %dB control character", bytes);
-    return -1;
   }
   // check *before ncplane_cursor_move_yx()* whether we're past the end of the
   // line. if scrolling is enabled, move to the next line if so. if x or y are
@@ -1880,13 +1883,32 @@ ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
   }
   targ->stylemask = stylemask;
   targ->channels = channels;
-  if(cell_load_direct(n, targ, egc, bytes, cols) < 0){
-    return -1;
+  // tabs get replaced with spaces, up to the next tab stop. we always try to
+  // write at least one. if there are no more tabstops on the current line, if
+  // autogrowing to the right, extend as necessary. otherwise, if scrolling,
+  // move to the next line. otherwise, simply fill any spaces we can. this has
+  // already taken place by the time we get here, if it ought have happened.
+  if(*egc == '\t'){
+    cols = TABSTOP - (n->x % TABSTOP);
+    if(n->x + 1 >= n->lenx){
+      if(!n->scrolling && n->autogrow){
+        ncplane_resize_simple(n, n->leny, n->lenx + (cols ? cols - 1 : TABSTOP - 1));
+        // must refresh targ; resize invalidated it
+        targ = ncplane_cell_ref_yx(n, n->y, n->x);
+      }
+    }
+    if(cell_load_direct(n, targ, " ", bytes, 1) < 0){
+      return -1;
+    }
+  }else{
+    if(cell_load_direct(n, targ, egc, bytes, cols) < 0){
+      return -1;
+    }
   }
 //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
   if(*egc != '\n'){
     ++n->x;
+    // if wide, set our right hand columns wide, and check for further damage
     for(int i = 1 ; i < cols ; ++i){
       nccell* candidate = &n->fb[nfbcellidx(n, n->y, n->x)];
       int off = nccell_cols(candidate);
@@ -1894,9 +1916,15 @@ ncplane_put(ncplane* n, int y, int x, const char* egc, int cols,
       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 != '\t'){
+        candidate->channels = targ->channels;
+        candidate->stylemask = targ->stylemask;
+        candidate->width = targ->width;
+      }else{
+        if(cell_load_direct(n, candidate, " ", bytes, 1) < 0){
+          return -1;
+        }
+      }
       ++n->x;
     }
   }


Test patch:
diff --git a/src/tests/cell.cpp b/src/tests/cell.cpp
index 8da3390a5..6052e7d19 100644
--- a/src/tests/cell.cpp
+++ b/src/tests/cell.cpp
@@ -359,7 +359,7 @@ TEST_CASE("Cell") {
     nccell c = NCCELL_TRIVIAL_INITIALIZER;
     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, '\t'));
     CHECK(1 == nccell_load_char(n_, &c, '\n'));
     CHECK(1 == nccell_load_char(n_, &c, ' '));
   }
diff --git a/src/tests/plane.cpp b/src/tests/plane.cpp
index 826c5ddd2..7b3f7ef5a 100644
--- a/src/tests/plane.cpp
+++ b/src/tests/plane.cpp
@@ -1031,14 +1031,17 @@ TEST_CASE("Plane") {
     ncplane_destroy(n1);
   }
 
-  // you ought not be able to output control characters
+  // you ought not be able to output control characters, aside from tab, and
+  // newlines (when we're scrolling).
   SUBCASE("DenyControlASCII") {
     signed char c = 0;
     c = -1;
     do{
       ++c;
       if(c && !isprint(c)){
-        CHECK(0 > ncplane_putchar_yx(n_, 0, 0, c));
+        if(c != '\t'){
+          CHECK(0 > ncplane_putchar_yx(n_, 0, 0, c));
+        }
       }else{
         CHECK(1 == ncplane_putchar_yx(n_, 0, 0, c));
       }
diff --git a/src/tests/tabs.cpp b/src/tests/tabs.cpp
new file mode 100644
index 000000000..ba7f0814b
--- /dev/null
+++ b/src/tests/tabs.cpp
@@ -0,0 +1,153 @@
+#include <cstdlib>
+#include "main.h"
+
+constexpr int TABWIDTH = 8;
+
+TEST_CASE("TaBs") { // refreshing and delicious
+  auto nc_ = testing_notcurses();
+  if(!nc_){
+    return;
+  }
+  struct ncplane* n_ = notcurses_stdplane(nc_);
+  REQUIRE(n_);
+
+  SUBCASE("PutcTaB") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 10;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    CHECK(TABWIDTH == ncplane_putchar(n, '\t'));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == TABWIDTH);
+    for(unsigned i = 0 ; i < x ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  SUBCASE("PutXoffsetTaBs") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 10;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    for(int i = 0 ; i < TABWIDTH ; ++i){
+      CHECK(TABWIDTH - i == ncplane_putchar(n, '\t'));
+      ncplane_cursor_yx(n, &y, &x);
+      CHECK(y == 0);
+      CHECK(x == TABWIDTH);
+      CHECK(0 == ncplane_cursor_move_yx(n, 0, i + 1));
+    }
+    for(unsigned i = 0 ; i < x ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  SUBCASE("PutwcTaB") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 10;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    CHECK(TABWIDTH == ncplane_putwc(n, L'\t'));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == TABWIDTH);
+    for(unsigned i = 0 ; i < x ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  SUBCASE("PutCellTaB") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 10;
+    auto n = ncplane_create(n_, &nopts);
+    nccell c = NCCELL_CHAR_INITIALIZER('\t');
+    unsigned y, x;
+    CHECK(8 == ncplane_putc(n, &c));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == TABWIDTH);
+    for(unsigned i = 0 ; i < x ; ++i){
+      char* s = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(s);
+      CHECK(0 == strcmp(s, " "));
+      free(s);
+    }
+  }
+
+  SUBCASE("PutMultipleTaBs") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 80;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    CHECK(16 == ncplane_putstr(n, "\t\t"));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == 16);
+    for(unsigned i = 0 ; i < x ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  SUBCASE("PutRowOfTaBs") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = 80;
+    nopts.flags = NCPLANE_OPTION_VSCROLL;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    CHECK(80 == ncplane_putstr(n, "\t\t\t\t\t\t\t\t\t\t"));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == 80);
+    for(unsigned i = 0 ; i < ncplane_dim_x(n) ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  SUBCASE("GrowWithTaBs") {
+    struct ncplane_options nopts{};
+    nopts.rows = 2;
+    nopts.cols = TABWIDTH;
+    nopts.flags = NCPLANE_OPTION_AUTOGROW;
+    auto n = ncplane_create(n_, &nopts);
+    unsigned y, x;
+    CHECK(16 == ncplane_putstr(n, "\t\t"));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(y == 0);
+    CHECK(x == 16);
+    CHECK(16 == ncplane_dim_x(n));
+    CHECK(8 == ncplane_putstr(n, "\t"));
+    ncplane_cursor_yx(n, &y, &x);
+    CHECK(x == 24);
+    CHECK(24 == ncplane_dim_x(n));
+    for(unsigned i = 0 ; i < ncplane_dim_x(n) ; ++i){
+      char* c = ncplane_at_yx(n, 0, i, nullptr, nullptr);
+      REQUIRE(c);
+      CHECK(0 == strcmp(c, " "));
+      free(c);
+    }
+  }
+
+  CHECK(0 == notcurses_stop(nc_));
+
+}