Repository: dankamongmen/notcurses (language: python)

Issue:
tfman: add structural browser
`ncman` should take the section+subsection structure and stick it into an `nctree`. this `nctree` will be anchored at the bottom. it ought be able to toggle the `nctree`'s visibility via some key. some other key ought allow navigation using this nctree. when we navigate through the page not using this tree, it ought still advance the tree, keeping it up to date with what's closest to visible.
tfman: add structural browser
`ncman` should take the section+subsection structure and stick it into an `nctree`. this `nctree` will be anchored at the bottom. it ought be able to toggle the `nctree`'s visibility via some key. some other key ought allow navigation using this nctree. when we navigate through the page not using this tree, it ought still advance the tree, keeping it up to date with what's closest to visible.
tfman: add structural browser
`ncman` should take the section+subsection structure and stick it into an `nctree`. this `nctree` will be anchored at the bottom. it ought be able to toggle the `nctree`'s visibility via some key. some other key ought allow navigation using this nctree. when we navigate through the page not using this tree, it ought still advance the tree, keeping it up to date with what's closest to visible.
tfman: add structural browser
`ncman` should take the section+subsection structure and stick it into an `nctree`. this `nctree` will be anchored at the bottom. it ought be able to toggle the `nctree`'s visibility via some key. some other key ought allow navigation using this nctree. when we navigate through the page not using this tree, it ought still advance the tree, keeping it up to date with what's closest to visible.


Discussion / hints:
i've written the necessary hookup code; we need implement `nctree_add()` now (#2458).
some progress has been made
notes:
 * ~~they're in the wrong order following the title -- error in `nctree_add()`?~~
 * ~~make the plane transparent wherever there's no text~~
 * left/right move between main browser and structure browser
 * ~~update structure browser focus as we move through the regular browser~~
 * ~~colorize based on distance from focus~~
 * throw a perimeter around the actual text
 * ~~add a key to toggle the structure menu's visibility~~
```
  ▁▁▁▁▁▁▁                                                                                                                           
 ▕ECRAPPO▏                                                                                                                          
 ▁🮀🮀🭨NAME▏                                                                                                                          
▕SYNOPSIS▏     
```
ok, lots of progress. we can't yet drive from the structure browser, but that's about it. we definitely want a perimeter or some other means to set it off from the rest.
i'm retarded. rather than this being a vertical tree, it ought be a ribbon. that way there's no modality, because left/right move along the ribbon, and up/down move within the page. duh. yeah, this was obviously the thing to do. stupid!
yeah, we'll do this in `dankamongmen/tfman-horizontal-structure`
so for the horizontal bar...ideally we would show all on-screen topics, right? but we might not have room. so instead, we want whatever's in the center of the screen to be in the center of the bar. except then we might never be able to show the extrema. so how about: when we've most recently moved down, we show the bottommost section on the rightmost part of the bar, and then whatever else can fit. when we've most recently moved up (and when we start), we show the topmost section on the leftmost part of the bar, and then whatever else can fit. we always keep some minimum spacing between elements, and expand the spacing if there are too few elements. we show partial elements, so the amount of spacing never need change.
i've written the necessary hookup code; we need implement `nctree_add()` now (#2458).
some progress has been made
notes:
 * ~~they're in the wrong order following the title -- error in `nctree_add()`?~~
 * ~~make the plane transparent wherever there's no text~~
 * left/right move between main browser and structure browser
 * ~~update structure browser focus as we move through the regular browser~~
 * ~~colorize based on distance from focus~~
 * throw a perimeter around the actual text
 * ~~add a key to toggle the structure menu's visibility~~
```
  ▁▁▁▁▁▁▁                                                                                                                           
 ▕ECRAPPO▏                                                                                                                          
 ▁🮀🮀🭨NAME▏                                                                                                                          
▕SYNOPSIS▏     
```
ok, lots of progress. we can't yet drive from the structure browser, but that's about it. we definitely want a perimeter or some other means to set it off from the rest.
i'm retarded. rather than this being a vertical tree, it ought be a ribbon. that way there's no modality, because left/right move along the ribbon, and up/down move within the page. duh. yeah, this was obviously the thing to do. stupid!
yeah, we'll do this in `dankamongmen/tfman-horizontal-structure`
so for the horizontal bar...ideally we would show all on-screen topics, right? but we might not have room. so instead, we want whatever's in the center of the screen to be in the center of the bar. except then we might never be able to show the extrema. so how about: when we've most recently moved down, we show the bottommost section on the rightmost part of the bar, and then whatever else can fit. when we've most recently moved up (and when we start), we show the topmost section on the leftmost part of the bar, and then whatever else can fit. we always keep some minimum spacing between elements, and expand the spacing if there are too few elements. we show partial elements, so the amount of spacing never need change.
i've written the necessary hookup code; we need implement `nctree_add()` now (#2458).
some progress has been made
notes:
 * ~~they're in the wrong order following the title -- error in `nctree_add()`?~~
 * ~~make the plane transparent wherever there's no text~~
 * left/right move between main browser and structure browser
 * ~~update structure browser focus as we move through the regular browser~~
 * ~~colorize based on distance from focus~~
 * throw a perimeter around the actual text
 * ~~add a key to toggle the structure menu's visibility~~
```
  ▁▁▁▁▁▁▁                                                                                                                           
 ▕ECRAPPO▏                                                                                                                          
 ▁🮀🮀🭨NAME▏                                                                                                                          
▕SYNOPSIS▏     
```
ok, lots of progress. we can't yet drive from the structure browser, but that's about it. we definitely want a perimeter or some other means to set it off from the rest.
i'm retarded. rather than this being a vertical tree, it ought be a ribbon. that way there's no modality, because left/right move along the ribbon, and up/down move within the page. duh. yeah, this was obviously the thing to do. stupid!
yeah, we'll do this in `dankamongmen/tfman-horizontal-structure`
so for the horizontal bar...ideally we would show all on-screen topics, right? but we might not have room. so instead, we want whatever's in the center of the screen to be in the center of the bar. except then we might never be able to show the extrema. so how about: when we've most recently moved down, we show the bottommost section on the rightmost part of the bar, and then whatever else can fit. when we've most recently moved up (and when we start), we show the topmost section on the leftmost part of the bar, and then whatever else can fit. we always keep some minimum spacing between elements, and expand the spacing if there are too few elements. we show partial elements, so the amount of spacing never need change.
i've written the necessary hookup code; we need implement `nctree_add()` now (#2458).
some progress has been made
notes:
 * ~~they're in the wrong order following the title -- error in `nctree_add()`?~~
 * ~~make the plane transparent wherever there's no text~~
 * left/right move between main browser and structure browser
 * ~~update structure browser focus as we move through the regular browser~~
 * ~~colorize based on distance from focus~~
 * throw a perimeter around the actual text
 * ~~add a key to toggle the structure menu's visibility~~
```
  ▁▁▁▁▁▁▁                                                                                                                           
 ▕ECRAPPO▏                                                                                                                          
 ▁🮀🮀🭨NAME▏                                                                                                                          
▕SYNOPSIS▏     
```
ok, lots of progress. we can't yet drive from the structure browser, but that's about it. we definitely want a perimeter or some other means to set it off from the rest.
i'm retarded. rather than this being a vertical tree, it ought be a ribbon. that way there's no modality, because left/right move along the ribbon, and up/down move within the page. duh. yeah, this was obviously the thing to do. stupid!
yeah, we'll do this in `dankamongmen/tfman-horizontal-structure`
so for the horizontal bar...ideally we would show all on-screen topics, right? but we might not have room. so instead, we want whatever's in the center of the screen to be in the center of the bar. except then we might never be able to show the extrema. so how about: when we've most recently moved down, we show the bottommost section on the rightmost part of the bar, and then whatever else can fit. when we've most recently moved up (and when we start), we show the topmost section on the leftmost part of the bar, and then whatever else can fit. we always keep some minimum spacing between elements, and expand the spacing if there are too few elements. we show partial elements, so the amount of spacing never need change.

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index be31fbcb28..53dfbe80c5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -992,7 +992,6 @@ list(FILTER TESTDATA EXCLUDE REGEX ".*osp$")
 install(FILES ${TESTDATA} DESTINATION ${CMAKE_INSTALL_DATADIR}/notcurses)
 endif()
 
-list(FILTER MANPAGES1 EXCLUDE REGEX "tfman.1")
 install(FILES ${MANPAGES1} DESTINATION ${CMAKE_INSTALL_DATADIR}/man/man1)
 install(FILES ${MANPAGES3} DESTINATION ${CMAKE_INSTALL_DATADIR}/man/man3)
 file(GLOB MARKDOWN CONFIGURE_DEPENDS *.md)
@@ -1003,9 +1002,9 @@ if(BUILD_EXECUTABLES)
 install(TARGETS notcurses-demo DESTINATION bin)
 install(TARGETS notcurses-info DESTINATION bin)
 install(TARGETS ncneofetch DESTINATION bin)
-#if(NOT WIN32)
-#install(TARGETS tfman DESTINATION bin)
-#endif()
+if(NOT WIN32)
+install(TARGETS tfman DESTINATION bin)
+endif()
 if(${USE_CXX})
 install(TARGETS notcurses-input DESTINATION bin)
 install(TARGETS nctetris DESTINATION bin)
diff --git a/README.md b/README.md
index 5e1fc71f87..0561a80a57 100644
--- a/README.md
+++ b/README.md
@@ -82,8 +82,8 @@ Why use this non-standard library?
   and transparent regions. All APIs natively support 24-bit color, quantized
   down as necessary for the terminal.
 
-* Portable support for bitmapped graphics, whether using Sixel, Kitty, the iTerm2
-  protocol, or even the Linux framebuffer console.
+* Portable support for bitmapped graphics, using Sixel, Kitty,
+  and even the Linux framebuffer console.
 
 * Support for unambiguous [keyboard protocols](https://sw.kovidgoyal.net/kitty/keyboard-protocol/).
 
@@ -138,7 +138,7 @@ others are external.
 
 ## Included tools
 
-Nine binaries are installed as part of Notcurses:
+Nine executables are installed as part of Notcurses:
 * `ncls`: an `ls` that displays multimedia in the terminal
 * `ncneofetch`: a [neofetch](https://github.com/dylanaraps/neofetch) ripoff
 * `ncplayer`: renders visual media (images/videos)
@@ -147,6 +147,7 @@ Nine binaries are installed as part of Notcurses:
 * `notcurses-info`: detect and print terminal capabilities/diagnostics
 * `notcurses-input`: decode and print keypresses
 * `notcurses-tester`: unit testing
+* `tfman`: a swank manual browser
 
 To run `notcurses-demo` from a checkout, provide the `data` directory via
 the `-p` argument. Demos requiring data files will otherwise abort. The base
@@ -276,10 +277,10 @@ If things break or seem otherwise lackluster, **please** consult the
 <details>
   <summary>We're paying by the electron, and have no C++ compiler. Can we still
   enjoy Notcurses goodness?</summary>
-  Some of it! You won't be able to build several binaries, nor the NCPP C++
+  Some of it! You won't be able to build several executables, nor the NCPP C++
   wrappers, nor can you build with the OpenImageIO multimedia backend (OIIO
   ships C++ headers). You'll be able to build the main library, though, as
-  well as <code>notcurses-demo</code> (and maybe a few other binaries).
+  well as <code>notcurses-demo</code> (and maybe a few other programs).
   Use <code>-DUSE_CXX=off</code>.
 </details>
 
diff --git a/doc/man/man1/tfman.1.md b/doc/man/man1/tfman.1.md
index cabf5c4960..7043135507 100644
--- a/doc/man/man1/tfman.1.md
+++ b/doc/man/man1/tfman.1.md
@@ -41,10 +41,11 @@ The following keypresses are recognized:
 * **q**: Quit.
 * **k**/**up**: Move up by one line.
 * **b**/**pgup**: Move up by one page.
+* **h**/**left**: Move up by one section.
 * **j**/**down**: Move down by one line.
 * **f**/**pgdown**: Move down by one page.
+* **l**/**right**: Move down by one section.
 * **s**: Toggle the structure browser's visibility.
-* **Tab**: Move between the page and structure browser.
 
 The mouse wheel can also be used to move up and down within the active browser.
 
diff --git a/src/input/input.cpp b/src/input/input.cpp
index 7b3a37a748..468777863e 100644
--- a/src/input/input.cpp
+++ b/src/input/input.cpp
@@ -125,7 +125,7 @@ const char* nckeystr(char32_t spkey){
     case NCKEY_EXIT:    return "exit";
     case NCKEY_PRINT:   return "print";
     case NCKEY_REFRESH: return "refresh";
-    case NCKEY_SEPARATOR: return "seperator";
+    case NCKEY_SEPARATOR: return "separator";
     case NCKEY_CAPS_LOCK: return "caps lock";
     case NCKEY_SCROLL_LOCK: return "scroll lock";
     case NCKEY_NUM_LOCK: return "num lock";
diff --git a/src/lib/blit.c b/src/lib/blit.c
index e211b43a2e..a5401d8925 100644
--- a/src/lib/blit.c
+++ b/src/lib/blit.c
@@ -991,7 +991,7 @@ const char* notcurses_str_blitter(ncblitter_e blitfxn){
 
 int ncblit_bgrx(const void* data, int linesize, const struct ncvisual_options* vopts){
   if(vopts->leny <= 0 || vopts->lenx <= 0){
-    logerror("invalid lenghts %u %u\n", vopts->leny, vopts->lenx);
+    logerror("invalid lengths %u %u\n", vopts->leny, vopts->lenx);
     return -1;
   }
   if(vopts->n == NULL){
@@ -1037,7 +1037,7 @@ int ncblit_rgb_packed(const void* data, int linesize,
 
 int ncblit_rgba(const void* data, int linesize, const struct ncvisual_options* vopts){
   if(vopts->leny <= 0 || vopts->lenx <= 0){
-    logerror("invalid lenghts %u %u\n", vopts->leny, vopts->lenx);
+    logerror("invalid lengths %u %u\n", vopts->leny, vopts->lenx);
     return -1;
   }
   if(vopts->n == NULL){
diff --git a/src/man/main.c b/src/man/main.c
index 297e8c52b7..719c3151b8 100644
--- a/src/man/main.c
+++ b/src/man/main.c
@@ -302,7 +302,7 @@ draw_domnode(struct ncplane* p, const pagedom* dom, const pagenode* n,
   ncplane_cursor_yx(p, &y, NULL);
   switch(n->ttype->ltype){
     case LINE_TH:
-      if(docstructure_add(dom->ds, dom->title, ncplane_y(p), DOCSTRUCTURE_TITLE, y)){
+      if(docstructure_add(dom->ds, dom->title, y)){
         return -1;
       }
       /*
@@ -312,7 +312,7 @@ draw_domnode(struct ncplane* p, const pagedom* dom, const pagenode* n,
       ncplane_set_styles(p, NCSTYLE_NONE);
       */break;
     case LINE_SH: // section heading
-      if(docstructure_add(dom->ds, n->text, ncplane_y(p), DOCSTRUCTURE_SECTION, y)){
+      if(docstructure_add(dom->ds, n->text, y)){
         return -1;
       }
       if(strcmp(n->text, "NAME")){
@@ -325,7 +325,7 @@ draw_domnode(struct ncplane* p, const pagedom* dom, const pagenode* n,
       }
       break;
     case LINE_SS: // subsection heading
-      if(docstructure_add(dom->ds, n->text, ncplane_y(p), DOCSTRUCTURE_SUBSECTION, y)){
+      if(docstructure_add(dom->ds, n->text, y)){
         return -1;
       }
       ncplane_puttext(p, -1, NCALIGN_LEFT, "\n\n", &b);
@@ -414,8 +414,9 @@ render_troff(struct notcurses* nc, const unsigned char* map, size_t mlen,
   }
   // this is just an estimate
   struct ncplane_options popts = {
-    .rows = dimy - 1,
+    .rows = dimy - 2,
     .cols = dimx,
+    .y = 1,
     .userptr = dom,
     .resizecb = resize_pman,
     .flags = NCPLANE_OPTION_AUTOGROW | NCPLANE_OPTION_VSCROLL,
@@ -432,8 +433,8 @@ render_troff(struct notcurses* nc, const unsigned char* map, size_t mlen,
   return pman;
 }
 
-static const char USAGE_TEXT[] = "⎥⇆/s⎢⎥b⇞k↑↓j⇟f⎢ (q)uit";
-static const char USAGE_TEXT_ASCII[] = "(tab/s) (bkjf) (q)uit";
+static const char USAGE_TEXT[] = "⎥h←s→l⎢⎥b⇞k↑↓j⇟f⎢ (q)uit";
+static const char USAGE_TEXT_ASCII[] = "(hsl) (bkjf) (q)uit";
 
 static int
 draw_bar(struct ncplane* bar, pagedom* dom){
@@ -554,6 +555,8 @@ manloop(struct notcurses* nc, const char* arg, unsigned noui){
   }
   uint32_t key;
   do{
+    bool movedown = false;
+    int newy = ncplane_y(page);
     ncinput ni;
     key = notcurses_get(nc, NULL, &ni);
     if(ni.evtype == NCTYPE_RELEASE){
@@ -568,42 +571,44 @@ manloop(struct notcurses* nc, const char* arg, unsigned noui){
       case 's':
         docstructure_toggle(page, bar, dom.ds);
         break;
-      case NCKEY_TAB: case L'\u21c6':
-        // FIXME switch between browsers
+      case 'h': case NCKEY_LEFT:
+        newy = docstructure_prev(dom.ds);
+        break;
+      case 'l': case NCKEY_RIGHT:
+        newy = docstructure_next(dom.ds);
+        movedown = true;
         break;
       case 'k': case NCKEY_UP:
-        if(ncplane_y(page)){
-          ncplane_move_rel(page, 1, 0);
-        }
+        newy = ncplane_y(page) + 1;
         break;
       // we can move down iff our last line is beyond the visible area
       case 'j': case NCKEY_DOWN:
-        if(ncplane_y(page) + ncplane_dim_y(page) >= ncplane_dim_y(stdn)){
-          ncplane_move_rel(page, -1, 0);
-        }
+        newy = ncplane_y(page) - 1;
+        movedown = true;
         break;
       case 'b': case NCKEY_PGUP:{
-        int newy = ncplane_y(page) + (int)ncplane_dim_y(stdn);
-        if(newy > 0){
-          newy = 0;
-        }
-        ncplane_move_yx(page, newy, 0);
+        newy = ncplane_y(page) + (int)ncplane_dim_y(stdn);
         break;
       }case 'f': case NCKEY_PGDOWN:{
-        int newy = ncplane_y(page) - (int)ncplane_dim_y(stdn) + 1;
-        if(newy + (int)ncplane_dim_y(page) < (int)ncplane_dim_y(stdn)){
-          newy += (int)ncplane_dim_y(stdn) - (newy + (int)ncplane_dim_y(page)) - 1;
-        }
-        ncplane_move_yx(page, newy, 0);
+        newy = ncplane_y(page) - (int)ncplane_dim_y(stdn) + 1;
+        movedown = true;
         break;
       }case 'q':
         ret = 0;
         goto done;
     }
-    int newy = ncplane_y(page);
-    docstructure_move(dom.ds, newy);
-    if(notcurses_render(nc)){
-      goto done;
+    if(newy > 1){
+      newy = 1;
+    }
+    if(newy + (int)ncplane_dim_y(page) < (int)ncplane_dim_y(stdn)){
+      newy += (int)ncplane_dim_y(stdn) - (newy + (int)ncplane_dim_y(page)) - 1;
+    }
+    if(newy != ncplane_y(page)){
+      ncplane_move_yx(page, newy, 0);
+      docstructure_move(dom.ds, newy, movedown);
+      if(notcurses_render(nc)){
+        goto done;
+      }
     }
   }while(key != (uint32_t)-1);
 
diff --git a/src/man/structure.c b/src/man/structure.c
index f818ad85a7..eac2192c13 100644
--- a/src/man/structure.c
+++ b/src/man/structure.c
@@ -2,69 +2,83 @@
 #include <notcurses/notcurses.h>
 #include "structure.h"
 
-#define HIERARCHY_MAX 3
-
 typedef struct docnode {
   char* title;
-  int line;
-  docstruct_e level;
   int y;
 } docnode;
 
 typedef struct docstructure {
-  struct nctree* nct;
-  // one entry for each hierarchy level + terminator, only used during creation
-  unsigned curpath[HIERARCHY_MAX + 1];
+  struct ncplane* n;
+  unsigned count;
+  docnode** nodes;
+  unsigned curnode;
   int cury;
   bool visible;
+  bool movedown;     // our last move was down
 } docstructure;
 
 static int
-docstruct_callback(struct ncplane* n, void* curry, int i){
-  if(n){
-    docnode* dn = curry;
-    uint64_t channels =
-      NCCHANNELS_INITIALIZER(0xff, 0xff, 0xff, 0x49,
-                             (0x9d - 0x8 * i), (0x9d + 0x8 * i));
-    ncplane_set_channels(n, channels);
-    ncplane_erase(n);
-    ncplane_putstr_aligned(n, 0, NCALIGN_RIGHT, dn->title);
-    ncplane_resize_simple(n, 1, ncplane_dim_x(n));
-    (void)i;
+docbar_redraw(docstructure *ds){
+  ncplane_erase(ds->n);
+  for(unsigned c = ds->curnode ; c < ds->count ; ++c){
+    if(c == ds->curnode){
+      ncplane_set_styles(ds->n, NCSTYLE_BOLD);
+      ncplane_set_fg_rgb(ds->n, 0x00ffb0);
+    }
+    if(ncplane_putstr(ds->n, ds->nodes[c]->title) <= 0){
+      return 0;
+    }
+    ncplane_set_styles(ds->n, NCSTYLE_NONE);
+    ncplane_set_fg_rgb(ds->n, 0x00b0b0);
+    if(notcurses_canutf8(ncplane_notcurses(ds->n))){
+      if(ncplane_putstr(ds->n, " ∎ ") <= 0){
+        return 0;
+      }
+    }else{
+      if(ncplane_putstr(ds->n, " | ") <= 0){
+        return 0;
+      }
+    }
   }
   return 0;
 }
 
+static int
+docbar_resize(struct ncplane* n){
+  unsigned width = ncplane_dim_x(ncplane_parent(n));
+  if(ncplane_resize_simple(n, 1, width)){
+    return -1;
+  }
+  return docbar_redraw(ncplane_userptr(n));
+}
+
 docstructure* docstructure_create(struct ncplane* n){
   docstructure* ds = malloc(sizeof(*ds));
   if(ds == NULL){
     return NULL;
   }
-  const int ROWS = 7;
-  const int COLDIV = 4;
   ncplane_options nopts = {
-    .rows = ROWS,
-    .cols = ncplane_dim_x(n) / COLDIV,
-    .y = ncplane_dim_y(n) - ROWS - 1,
-    .x = ncplane_dim_x(n) - (ncplane_dim_x(n) / COLDIV) - 1,
-    .flags = NCPLANE_OPTION_AUTOGROW, // autogrow to right
+    .rows = 1,
+    .cols = ncplane_dim_x(n),
+    .y = 0,
+    .userptr = ds,
+    .resizecb = docbar_resize,
   };
   struct ncplane* p = ncplane_create(n, &nopts);
   if(p == NULL){
     return NULL;
   }
-  nctree_options topts = {
-    .nctreecb = docstruct_callback,
-  };
-  if((ds->nct = nctree_create(p, &topts)) == NULL){
-    free(ds);
-    return NULL;
-  }
-  for(unsigned z = 0 ; z < sizeof(ds->curpath) / sizeof(*ds->curpath) ; ++z){
-    ds->curpath[z] = UINT_MAX;
-  }
+  uint64_t channels = NCCHANNELS_INITIALIZER(0x0, 0xb0, 0xb0, 0x06, 0x11, 0x2f);
+  ncplane_set_base(p, " ", 0, channels);
+  ncplane_set_channels(p, channels);
+  ds->n = p;
   ds->visible = true;
+  ds->count = 0;
+  ds->nodes = NULL;
+  ds->movedown = false;
   ds->cury = 0;
+  ds->curnode = 0;
+  docbar_redraw(ds);
   return ds;
 }
 
@@ -80,13 +94,6 @@ void docstructure_toggle(struct ncplane* p, struct ncplane* b, docstructure* ds)
   }
 }
 
-void docstructure_free(docstructure* ds){
-  if(ds){
-    nctree_destroy(ds->nct);
-    free(ds);
-  }
-}
-
 static void
 docnode_free(docnode* dn){
   if(dn){
@@ -95,8 +102,18 @@ docnode_free(docnode* dn){
   }
 }
 
+void docstructure_free(docstructure* ds){
+  if(ds){
+    while(ds->count--){
+      docnode_free(ds->nodes[ds->count]);
+    }
+    free(ds->nodes);
+    free(ds);
+  }
+}
+
 static docnode*
-docnode_create(const char* title, int line, docstruct_e level, int y){
+docnode_create(const char* title, int y){
   docnode* dn = malloc(sizeof(*dn));
   if(dn == NULL){
     return NULL;
@@ -105,53 +122,52 @@ docnode_create(const char* title, int line, docstruct_e level, int y){
     free(dn);
     return NULL;
   }
-  dn->level = level;
-  dn->line = line;
   dn->y = -y;
   return dn;
 }
 
 // add the specified [sub]section to the document strucure. |line| refers to
 // the row on the display plane, *not* the line in the original content.
-int docstructure_add(docstructure* ds, const char* title, int line,
-                     docstruct_e level, int y){
-  unsigned addpath[sizeof(ds->curpath) / sizeof(*ds->curpath)];
-  if(level < 0 || (unsigned)level >= sizeof(addpath) / sizeof(*addpath) - 1){
-    fprintf(stderr, "invalid level %d\n", level);
-    return -1;
-  }
-  docnode* dn = docnode_create(title, line, level, y);
+int docstructure_add(docstructure* ds, const char* title, int y){
+  docnode* dn = docnode_create(title, y);
   if(dn == NULL){
     return -1;
   }
-  unsigned z = 0;
-  while(z <= level){
-    if((addpath[z] = ds->curpath[z]) == UINT_MAX){
-      ds->curpath[z] = 0;
-      addpath[z] = 0;
-    }else if(z == level){
-      addpath[z] = ++ds->curpath[z];
-    }
-    ++z;
-  }
-  addpath[z] = UINT_MAX;
-  struct nctree_item nitem = {
-    .curry = dn,
-  };
-  if(nctree_add(ds->nct, addpath, &nitem)){
+  unsigned newcount = ds->count + 1;
+  docnode** newdn = realloc(ds->nodes, newcount * sizeof(*ds->nodes));
+  if(newdn == NULL){
     docnode_free(dn);
     return -1;
   }
-  ds->curpath[z] = addpath[z];
-  if(nctree_redraw(ds->nct)){
+  ds->nodes = newdn;
+  ds->nodes[ds->count] = dn;
+  ds->count = newcount;
+  if(docbar_redraw(ds)){
     return -1;
   }
   return 0;
 }
 
-int docstructure_move(docstructure* ds, int newy){
+// returns corresponding y
+int docstructure_prev(docstructure* ds){
+  if(ds->curnode){
+    --ds->curnode;
+  }
+  return ds->nodes[ds->curnode]->y;
+}
+
+int docstructure_next(docstructure* ds){
+  if(ds->curnode + 1 < ds->count){
+    ++ds->curnode;
+  }
+  return ds->nodes[ds->curnode]->y;
+}
+
+int docstructure_move(docstructure* ds, int newy, unsigned movedown){
   docnode* pdn = NULL;
   docnode* dn;
+  ds->movedown = movedown;
+  /* FIXME
   if(newy > ds->cury){
     while((dn = nctree_prev(ds->nct)) != pdn){
       if(dn->y > newy){
@@ -169,8 +185,9 @@ int docstructure_move(docstructure* ds, int newy){
       pdn = dn;
     }
   }
+  */
   ds->cury = newy;
-  if(nctree_redraw(ds->nct)){
+  if(docbar_redraw(ds)){
     return -1;
   }
   return 0;
diff --git a/src/man/structure.h b/src/man/structure.h
index f00f0bdd1d..d88d56054c 100644
--- a/src/man/structure.h
+++ b/src/man/structure.h
@@ -8,24 +8,20 @@ extern "C" {
 struct ncplane;
 struct docstructure;
 
-typedef enum {
-  DOCSTRUCTURE_TITLE,
-  DOCSTRUCTURE_SECTION,
-  DOCSTRUCTURE_SUBSECTION,
-} docstruct_e;
-
 struct docstructure* docstructure_create(struct ncplane* n);
 void docstructure_free(struct docstructure* ds);
 
 void docstructure_toggle(struct ncplane* p, struct ncplane* b, struct docstructure *ds);
 
-// add the specified [sub]section to the document strucure. |line| refers to
-// the row on the display plane, *not* the line in the original content.
-int docstructure_add(struct docstructure* ds, const char* title, int line,
-                     docstruct_e level, int y);
+// add the specified [sub]section to the document strucure.
+int docstructure_add(struct docstructure* ds, const char* title, int y);
 
 // update the docstructure browser based off a move of the page plane from to |newy|.
-int docstructure_move(struct docstructure* ds, int newy);
+// |movedown| ought be non-zero iff the move was down.
+int docstructure_move(struct docstructure* ds, int newy, unsigned movedown);
+
+int docstructure_prev(struct docstructure* ds);
+int docstructure_next(struct docstructure* ds);
 
 #ifdef __cplusplus
 }


Test patch:
diff --git a/src/tests/textlayout.cpp b/src/tests/textlayout.cpp
index 758e96a874..6197e9eee1 100644
--- a/src/tests/textlayout.cpp
+++ b/src/tests/textlayout.cpp
@@ -620,7 +620,16 @@ TEST_CASE("TextLayout") {
     CHECK(0 == notcurses_render(nc_));
     char* line = ncplane_contents(sp, 0, 0, 0, 0);
     REQUIRE(line);
-    CHECK(0 == strcmp(line, "Notcurses provides several widgets to quickly build vivid TUIs.This NCReader widget facilitates free-form text entry complete with readline-style bindings. NCSelector allows a single option to be selected from a list. NCMultiselector allows 0..n options to be selected from a list of n items. NCFdplane streams a file descriptor, while NCSubproc spawns a subprocess and streams its output. A variety of plots are supported, and menus can be placed along the top and/or bottom of any plane.Widgets can be controlled with the keyboard and/or mouse. They are implemented atop ncplanes, and these planes can be manipulated like all others."));
+    CHECK(0 == strcmp(line, "Notcurses provides several widgets to quickly build vivid TUIs."
+                            "This NCReader widget facilitates free-form text entry complete "
+                            "with readline-style bindings. NCSelector allows a single option "
+                            "to be selected from a list. NCMultiselector allows 0..n options "
+                            "to be selected from a list of n items. NCFdplane streams a file "
+                            "descriptor, while NCSubproc spawns a subprocess and streams its "
+                            "output. A variety of plots are supported, and menus can be placed "
+                            "along the top and/or bottom of any plane.Widgets can be controlled "
+                            "with the keyboard and/or mouse. They are implemented atop ncplanes, "
+                            "and these planes can be manipulated like all others."));
     free(line);
     CHECK(0 == ncplane_destroy(sp));
   }