Repository: dankamongmen/notcurses (language: python)

Issue:
add undercurl as a style
At least kitty and wezterm, and probably others, support undercurl. Add `NCSTYLE_UNDERCURL` and the necessary code to support it, where available.
add undercurl as a style
At least kitty and wezterm, and probably others, support undercurl. Add `NCSTYLE_UNDERCURL` and the necessary code to support it, where available.
add undercurl as a style
At least kitty and wezterm, and probably others, support undercurl. Add `NCSTYLE_UNDERCURL` and the necessary code to support it, where available.
add undercurl as a style
At least kitty and wezterm, and probably others, support undercurl. Add `NCSTYLE_UNDERCURL` and the necessary code to support it, where available.


Discussion / hints:
it looks like both wezterm and kitty are using `Smulx=\E[4\:%p1%dm`, and alacritty also has this defined, though not in the terminfo database entry *i* have for it, odd.

https://github.com/kovidgoyal/kitty/pull/3725
https://github.com/wez/wezterm/issues/415
So it looks like underline and undercurl (and double underline) are mutually exclusive.

That's actually good, because we want to be able to color the underline etc., which there's no way to do through the `set_styles()` interface. BUT we can add a function to configure the current underline state...except where would we then put this in rendered mode? it makes sense as a per-`nccell` thing...but we only have 4 bits free in the `nccell`.

hrmmmm. hrmmmmmmmmmmm.

for direct mode, it's trivial, since we just need a single current state, which we just pass through.
![2021-06-14-014502_1072x674_scrot](https://user-images.githubusercontent.com/143473/121844529-290e9a80-ccb2-11eb-934f-1ce7f694784f.png)

it looks like both wezterm and kitty are using `Smulx=\E[4\:%p1%dm`, and alacritty also has this defined, though not in the terminfo database entry *i* have for it, odd.

https://github.com/kovidgoyal/kitty/pull/3725
https://github.com/wez/wezterm/issues/415
So it looks like underline and undercurl (and double underline) are mutually exclusive.

That's actually good, because we want to be able to color the underline etc., which there's no way to do through the `set_styles()` interface. BUT we can add a function to configure the current underline state...except where would we then put this in rendered mode? it makes sense as a per-`nccell` thing...but we only have 4 bits free in the `nccell`.

hrmmmm. hrmmmmmmmmmmm.

for direct mode, it's trivial, since we just need a single current state, which we just pass through.
![2021-06-14-014502_1072x674_scrot](https://user-images.githubusercontent.com/143473/121844529-290e9a80-ccb2-11eb-934f-1ce7f694784f.png)

it looks like both wezterm and kitty are using `Smulx=\E[4\:%p1%dm`, and alacritty also has this defined, though not in the terminfo database entry *i* have for it, odd.

https://github.com/kovidgoyal/kitty/pull/3725
https://github.com/wez/wezterm/issues/415
So it looks like underline and undercurl (and double underline) are mutually exclusive.

That's actually good, because we want to be able to color the underline etc., which there's no way to do through the `set_styles()` interface. BUT we can add a function to configure the current underline state...except where would we then put this in rendered mode? it makes sense as a per-`nccell` thing...but we only have 4 bits free in the `nccell`.

hrmmmm. hrmmmmmmmmmmm.

for direct mode, it's trivial, since we just need a single current state, which we just pass through.
![2021-06-14-014502_1072x674_scrot](https://user-images.githubusercontent.com/143473/121844529-290e9a80-ccb2-11eb-934f-1ce7f694784f.png)

it looks like both wezterm and kitty are using `Smulx=\E[4\:%p1%dm`, and alacritty also has this defined, though not in the terminfo database entry *i* have for it, odd.

https://github.com/kovidgoyal/kitty/pull/3725
https://github.com/wez/wezterm/issues/415
So it looks like underline and undercurl (and double underline) are mutually exclusive.

That's actually good, because we want to be able to color the underline etc., which there's no way to do through the `set_styles()` interface. BUT we can add a function to configure the current underline state...except where would we then put this in rendered mode? it makes sense as a per-`nccell` thing...but we only have 4 bits free in the `nccell`.

hrmmmm. hrmmmmmmmmmmm.

for direct mode, it's trivial, since we just need a single current state, which we just pass through.
![2021-06-14-014502_1072x674_scrot](https://user-images.githubusercontent.com/143473/121844529-290e9a80-ccb2-11eb-934f-1ce7f694784f.png)


Fix patch:
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index ad9f64f83c..c84913dfc1 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -663,7 +663,10 @@ API int nccell_duplicate(struct ncplane* n, nccell* targ, const nccell* c);
 // Release resources held by the nccell 'c'.
 API void nccell_release(struct ncplane* n, nccell* c);
 
-#define NCSTYLE_MASK      0x03ffu
+#define NCSTYLE_MASK      0x07ffu
+#define NCSTYLE_UNDERCURL 0x0400u
+#define NCSTYLE_STRUCK    0x0200u
+#define NCSTYLE_ITALIC    0x0100u
 #define NCSTYLE_STANDOUT  0x0080u
 #define NCSTYLE_UNDERLINE 0x0040u
 #define NCSTYLE_REVERSE   0x0020u
@@ -672,8 +675,6 @@ API void nccell_release(struct ncplane* n, nccell* c);
 #define NCSTYLE_BOLD      0x0004u
 #define NCSTYLE_INVIS     0x0002u
 #define NCSTYLE_PROTECT   0x0001u
-#define NCSTYLE_ITALIC    0x0100u
-#define NCSTYLE_STRUCK    0x0200u
 #define NCSTYLE_NONE      0
 
 // Set the specified style bits for the nccell 'c', whether they're actively
diff --git a/src/lib/debug.c b/src/lib/debug.c
index 85f527ddea..ea43df9888 100644
--- a/src/lib/debug.c
+++ b/src/lib/debug.c
@@ -10,6 +10,41 @@ capbool(bool cap){
   return cap ? 'y' : 'n';
 }
 
+static void
+tinfo_debug_uline(const tinfo* ti, FILE* debugfp){
+  const char* smul = get_escape(ti, ESCAPE_SMUL);
+  if(smul){
+    term_emit(smul, debugfp, false);
+  }
+  fprintf(debugfp, "uline");
+  if(smul){
+    const char* rmul = get_escape(ti, ESCAPE_RMUL);
+    term_emit(rmul, debugfp, false);
+  }
+}
+
+static void
+tinfo_debug_ucurl(const tinfo* ti, FILE* debugfp){
+  const char* smulx = get_escape(ti, ESCAPE_SMULX);
+  if(smulx){
+    term_emit(smulx, debugfp, false);
+  }
+  fprintf(debugfp, "ucurl");
+  if(smulx){
+    const char* smulnox = get_escape(ti, ESCAPE_SMULNOX);
+    term_emit(smulnox, debugfp, false);
+  }
+}
+
+static void
+tinfo_debug_styles(const tinfo* ti, FILE* debugfp, const char* indent){
+  fprintf(debugfp, "%s", indent);
+  tinfo_debug_ucurl(ti, debugfp);
+  fprintf(debugfp, " ");
+  tinfo_debug_uline(ti, debugfp);
+  fprintf(debugfp, "\n");
+}
+
 static void
 tinfo_debug_caps(const tinfo* ti, FILE* debugfp, int rows, int cols,
                  unsigned images, unsigned videos){
@@ -36,6 +71,7 @@ tinfo_debug_caps(const tinfo* ti, FILE* debugfp, int rows, int cols,
   }else{
     fprintf(debugfp, "%srgba pixel graphics supported\n", indent);
   }
+  tinfo_debug_styles(ti, debugfp, indent);
   fprintf(debugfp, "%sutf8: %c quad: %c sex: %c braille: %c images: %c videos: %c\n",
           indent, capbool(ti->caps.utf8), capbool(ti->caps.quadrants),
           capbool(ti->caps.sextants), capbool(ti->caps.braille),
diff --git a/src/lib/direct.c b/src/lib/direct.c
index a4efc6cf47..21b7a73e81 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -918,6 +918,11 @@ int ncdirect_on_styles(ncdirect* n, unsigned stylebits){
                      get_escape(&n->tcache, ESCAPE_RMXX))){
       return -1;
     }
+    if(term_setstyle(n->ttyfp, n->stylemask, stylemask, NCSTYLE_UNDERCURL,
+                     get_escape(&n->tcache, ESCAPE_SMULX),
+                     get_escape(&n->tcache, ESCAPE_SMULNOX))){
+      return -1;
+    }
     n->stylemask = stylemask;
     return 0;
   }
@@ -946,6 +951,11 @@ int ncdirect_off_styles(ncdirect* n, unsigned stylebits){
                      get_escape(&n->tcache, ESCAPE_RMXX))){
       return -1;
     }
+    if(term_setstyle(n->ttyfp, n->stylemask, stylemask, NCSTYLE_UNDERCURL,
+                     get_escape(&n->tcache, ESCAPE_SMULX),
+                     get_escape(&n->tcache, ESCAPE_SMULNOX))){
+      return -1;
+    }
     n->stylemask = stylemask;
     return 0;
   }
@@ -976,6 +986,11 @@ int ncdirect_set_styles(ncdirect* n, unsigned stylebits){
                     get_escape(&n->tcache, ESCAPE_RMXX))){
     return -1;
   }
+  if(term_setstyle(n->ttyfp, n->stylemask, stylemask, NCSTYLE_UNDERCURL,
+                    get_escape(&n->tcache, ESCAPE_SMULX),
+                    get_escape(&n->tcache, ESCAPE_SMULNOX))){
+    return -1;
+  }
   n->stylemask = stylemask;
   return 0;
 }
diff --git a/src/lib/render.c b/src/lib/render.c
index 271921f4f5..e57e2a9ab1 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -616,6 +616,9 @@ term_setstyles(FILE* out, notcurses* nc, const nccell* c){
   ret |= term_setstyle(out, nc->rstate.curattr, cellattr, NCSTYLE_STRUCK,
                        get_escape(&nc->tcache, ESCAPE_SMXX),
                        get_escape(&nc->tcache, ESCAPE_RMXX));
+  ret |= term_setstyle(out, nc->rstate.curattr, cellattr, NCSTYLE_UNDERCURL,
+                       get_escape(&nc->tcache, ESCAPE_SMULX),
+                       get_escape(&nc->tcache, ESCAPE_SMULNOX));
   nc->rstate.curattr = cellattr;
   if(normalized){
     nc->rstate.fgdefelidable = true;
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index cd754ca6c7..d7cce6095f 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -85,10 +85,73 @@ match_termname(const char* termname, queried_terminals_e* qterm){
   return 0;
 }
 
+void free_terminfo_cache(tinfo* ti){
+  free(ti->esctable);
+  ncinputlayer_stop(&ti->input);
+}
+
+// tlen -- size of escape table. tused -- used bytes in same.
+// returns -1 if the starting location is >= 65535. otherwise,
+// copies tstr into the table, and sets up 1-biased index.
+static int
+grow_esc_table(tinfo* ti, const char* tstr, escape_e esc,
+               size_t* tlen, size_t* tused){
+  if(*tused >= 65535){ // we only have 16 bits for the index
+    fprintf(stderr, "Can't add escape %d to full table\n", esc);
+    return -1;
+  }
+  if(ti->escindices[esc] > 0){
+    fprintf(stderr, "Already defined escape %d (%s)\n",
+            esc, get_escape(ti, esc));
+    return -1;
+  }
+  size_t slen = strlen(tstr) + 1; // count the nul term
+  if(*tlen - *tused < slen){
+    // guaranteed to give us enough space to add tstr (and then some)
+    size_t newsize = *tlen + 4020 + slen; // don't pull two pages ideally
+    char* tmp = realloc(ti->esctable, newsize);
+    if(tmp == NULL){
+      return -1;
+    }
+    ti->esctable = tmp;
+    *tlen = newsize;
+  }
+  // we now are guaranteed sufficient space to copy tstr
+  memcpy(ti->esctable + *tused, tstr, slen);
+  ti->escindices[esc] = *tused + 1; // one-bias
+  *tused += slen;
+  return 0;
+}
+
+static int
+init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
+                  size_t* tablelen, size_t* tableused){
+  char* tstr;
+  if(terminfostr(&tstr, name) == 0){
+    if(grow_esc_table(ti, tstr, idx, tablelen, tableused)){
+      return -1;
+    }
+  }else{
+    ti->escindices[idx] = 0;
+  }
+  return 0;
+}
+
+static int
+add_smulx_escapes(tinfo* ti, size_t* tablelen, size_t* tableused){
+  if(grow_esc_table(ti, "\x1b[4:3m", ESCAPE_SMULX, tablelen, tableused) ||
+     grow_esc_table(ti, "\x1b[4:0m", ESCAPE_SMULNOX, tablelen, tableused)){
+    return -1;
+  }
+  ti->supported_styles |= NCSTYLE_UNDERCURL;
+  return 0;
+}
+
 // Qui si convien lasciare ogne sospetto; ogne viltà convien che qui sia morta.
 static int
 apply_term_heuristics(tinfo* ti, const char* termname, int fd,
-                      queried_terminals_e qterm){
+                      queried_terminals_e qterm,
+                      size_t* tablelen, size_t* tableused){
   if(!termname){
     // setupterm interprets a missing/empty TERM variable as the special value “unknown”.
     termname = "unknown";
@@ -106,6 +169,9 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd,
     ti->caps.quadrants = true;
     ti->caps.rgb = true;
     setup_kitty_bitmaps(ti, fd);
+    if(add_smulx_escapes(ti, tablelen, tableused)){
+      return -1;
+    }
   }else if(qterm == TERMINAL_ALACRITTY){
     termname = "Alacritty";
     ti->caps.quadrants = true;
@@ -131,6 +197,11 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd,
     const char* termver = getenv("TERM_PROGRAM_VERSION");
     if(termver && strcmp(termver, "20210610") >= 0){
       ti->caps.sextants = true; // good caps.sextants as of 2021-06-10
+      if(add_smulx_escapes(ti, tablelen, tableused)){
+        return -1;
+      }
+    }else{
+      termname = "XTerm";
     }
   }else if(qterm == TERMINAL_XTERM){
     termname = "XTerm";
@@ -158,52 +229,6 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd,
   return 0;
 }
 
-void free_terminfo_cache(tinfo* ti){
-  free(ti->esctable);
-  ncinputlayer_stop(&ti->input);
-}
-
-// tlen -- size of escape table. tused -- used bytes in same.
-// returns -1 if the starting location is >= 65535. otherwise,
-// copies tstr into the table, and sets up 1-biased index.
-static int
-grow_esc_table(tinfo* ti, const char* tstr, escape_e esc,
-               size_t* tlen, size_t* tused){
-  if(*tused >= 65535){
-    return -1;
-  }
-  size_t slen = strlen(tstr) + 1; // count the nul term
-  if(*tlen - *tused < slen){
-    // guaranteed to give us enough space to add tstr (and then some)
-    size_t newsize = *tlen + 4020 + slen; // don't pull two pages ideally
-    char* tmp = realloc(ti->esctable, newsize);
-    if(tmp == NULL){
-      return -1;
-    }
-    ti->esctable = tmp;
-    *tlen = newsize;
-  }
-  // we now are guaranteed sufficient space to copy tstr
-  memcpy(ti->esctable + *tused, tstr, slen);
-  ti->escindices[esc] = *tused + 1; // one-bias
-  *tused += slen;
-  return 0;
-}
-
-static int
-init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
-                  size_t* tablelen, size_t* tableused){
-  char* tstr;
-  if(terminfostr(&tstr, name) == 0){
-    if(grow_esc_table(ti, tstr, idx, tablelen, tableused)){
-      return -1;
-    }
-  }else{
-    ti->escindices[idx] = 0;
-  }
-  return 0;
-}
-
 // Device Attributes; replies with (depending on decTerminalID resource):
 //   ⇒  CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
 //   ⇒  CSI ? 1 ; 0 c  ("VT101 with No Options")
@@ -305,6 +330,8 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
     { ESCAPE_SMKX, "smkx", },
     { ESCAPE_SMXX, "smxx", },
     { ESCAPE_RMXX, "rmxx", },
+    { ESCAPE_SMUL, "smul", },
+    { ESCAPE_RMUL, "rmul", },
     { ESCAPE_SC, "sc", },
     { ESCAPE_RC, "rc", },
     { ESCAPE_CLEAR, "clear", },
@@ -360,9 +387,9 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
   }
   // we don't actually use the bold capability -- we use sgr exclusively.
   // but we use the presence of the bold capability to determine whether
-  // we think sgr supports bold, which...might be valid? i'm unsure. futher,
-  // some terminals cannot combine certain styles with colors. don't
-  // advertise support for the style in that case.
+  // we think sgr supports bold, which...might be valid? i'm unsure.
+  // further, some terminals cannot combine certain styles with colors.
+  // don't advertise support for the style in that case.
   const struct style {
     unsigned s;        // NCSTYLE_* value
     const char* tinfo; // terminfo capability for conditional permit
@@ -377,7 +404,6 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
     { NCSTYLE_ITALIC, "sitm", A_ITALIC },
     { NCSTYLE_INVIS, "invis", A_INVIS },
     { NCSTYLE_PROTECT, "prot", A_PROTECT },
-    { NCSTYLE_STRUCK, "smxx", 0 },
     { 0, NULL, 0 }
   };
   int nocolor_stylemask = tigetnum("ncv");
@@ -434,8 +460,7 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
       }
     }
   }
-//fprintf(stderr, "DETECTED TERM: %d\n", detected);
-  if(apply_term_heuristics(ti, termname, fd, detected)){
+  if(apply_term_heuristics(ti, termname, fd, detected, &tablelen, &tableused)){
     ncinputlayer_stop(&ti->input);
     goto err;
   }
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 9a57e482ba..dc44dbbeaf 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -49,6 +49,10 @@ typedef enum {
   ESCAPE_SMCUP,    // "smcup" enter alternate screen
   ESCAPE_RMCUP,    // "rmcup" leave alternate screen
   ESCAPE_SMXX,     // "smxx" start struckout
+  ESCAPE_SMUL,     // "smul" start underline
+  ESCAPE_RMUL,     // "rmul" end underline
+  ESCAPE_SMULX,    // "Smulx" deparameterized: start extended underline
+  ESCAPE_SMULNOX,  // "Smulx" deparameterized: kill underline
   ESCAPE_RMXX,     // "rmxx" end struckout
   ESCAPE_SC,       // "sc" push the cursor onto the stack
   ESCAPE_RC,       // "rc" pop the cursor off the stack
diff --git a/src/poc/sgr-direct.c b/src/poc/sgr-direct.c
index 1b6722fdb8..9ac7efd0dd 100644
--- a/src/poc/sgr-direct.c
+++ b/src/poc/sgr-direct.c
@@ -13,7 +13,7 @@ int main(void){
     return EXIT_FAILURE;
   }
   int e = 0;
-  for(unsigned i = 0 ; i < (NCSTYLE_STRUCK << 1u) ; ++i){
+  for(unsigned i = 0 ; i < (NCSTYLE_UNDERCURL << 1u) ; ++i){
     if((ncdirect_supported_styles(nc) & i) == i){
       if(ncdirect_set_styles(nc, i)){
         ncdirect_stop(nc);
diff --git a/src/poc/sgr-full.c b/src/poc/sgr-full.c
index aeae93484a..af39e74a6d 100644
--- a/src/poc/sgr-full.c
+++ b/src/poc/sgr-full.c
@@ -36,6 +36,8 @@ int main(void){
   ncplane_putstr_yx(n, y++, 0, "a ═ italic reverse");
   ncplane_set_styles(n, NCSTYLE_ITALIC | NCSTYLE_UNDERLINE);
   ncplane_putstr_yx(n, y++, 0, "a ═ italic underline");
+  ncplane_set_styles(n, NCSTYLE_ITALIC | NCSTYLE_UNDERCURL);
+  ncplane_putstr_yx(n, y++, 0, "a ═ italic undercurl");
   ncplane_set_styles(n, NCSTYLE_ITALIC | NCSTYLE_BLINK);
   ncplane_putstr_yx(n, y++, 0, "a ═ italic blink");
   ncplane_set_styles(n, NCSTYLE_ITALIC | NCSTYLE_STRUCK);
@@ -46,17 +48,24 @@ int main(void){
   ncplane_putstr_yx(n, y++, 0, "a ═ struck reverse");
   ncplane_set_styles(n, NCSTYLE_STRUCK | NCSTYLE_UNDERLINE);
   ncplane_putstr_yx(n, y++, 0, "a ═ struck underline");
+  ncplane_set_styles(n, NCSTYLE_STRUCK | NCSTYLE_UNDERCURL);
+  ncplane_putstr_yx(n, y++, 0, "a ═ struck undercurl");
   ncplane_set_styles(n, NCSTYLE_STRUCK | NCSTYLE_BLINK);
   ncplane_putstr_yx(n, y++, 0, "a ═ struck blink");
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_REVERSE);
   ncplane_putstr_yx(n, y++, 0, "a ═ bold reverse");
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_UNDERLINE);
   ncplane_putstr_yx(n, y++, 0, "a ═ bold underline");
+  ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_UNDERCURL);
+  ncplane_putstr_yx(n, y++, 0, "a ═ bold undercurl");
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_BLINK);
   ncplane_putstr_yx(n, y++, 0, "a ═ bold blink");
   ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_REVERSE | NCSTYLE_UNDERLINE |
                         NCSTYLE_BLINK | NCSTYLE_ITALIC | NCSTYLE_STRUCK);
   ncplane_putstr_yx(n, y++, 0, "a ═ whoomp! there it is");
+  ncplane_set_styles(n, NCSTYLE_BOLD | NCSTYLE_REVERSE | NCSTYLE_UNDERCURL |
+                        NCSTYLE_BLINK | NCSTYLE_ITALIC | NCSTYLE_STRUCK);
+  ncplane_putstr_yx(n, y++, 0, "a ═ whoomp! there it curls");
 
   ncplane_set_styles(n, NCSTYLE_NONE);
   if(notcurses_render(nc)){


Test patch:
diff --git a/src/tests/direct.cpp b/src/tests/direct.cpp
index a5075becb4..a8890cbfa3 100644
--- a/src/tests/direct.cpp
+++ b/src/tests/direct.cpp
@@ -43,6 +43,16 @@ TEST_CASE("Direct") {
     }
   }
 
+  SUBCASE("SetUndercurl") {
+    unsigned styles = ncdirect_supported_styles(nc_);
+    if(styles & NCSTYLE_UNDERCURL){
+      CHECK(0 == ncdirect_set_styles(nc_, NCSTYLE_UNDERCURL));
+      printf("DirectMode *undercurl*!\n");
+      fflush(stdout);
+      CHECK(0 == ncdirect_off_styles(nc_, NCSTYLE_UNDERCURL));
+    }
+  }
+
   SUBCASE("SetStruck") {
     unsigned styles = ncdirect_supported_styles(nc_);
     if(styles & NCSTYLE_STRUCK){