From 6c7c9be6d28e35f91adb4caea98a4b92beb50f1b Mon Sep 17 00:00:00 2001 From: nick black Date: Sat, 27 Feb 2021 14:52:16 -0500 Subject: [PATCH] require explicit check for pixel support Add `notcurses_check_pixel_support()` and `ncdirect_check_pixel_support()` per #1367. Removes NCOPTION_VERIFY_SIXEL, again per #1367. Adds `free_terminfo_cache()`, and calls it from both `notcurses_stop_minimal()` and `ncdirect_stop()`. Update all documentation. Closes #1371 and #1367. --- NEWS.md | 8 ++++++-- USAGE.md | 18 ++++++++++++------ doc/man/man3/notcurses_direct.3.md | 10 ++++++++++ doc/man/man3/notcurses_init.3.md | 7 ------- doc/man/man3/notcurses_visual.3.md | 12 ++++++++++++ include/ncpp/Direct.hh | 5 +++++ include/notcurses/direct.h | 5 +++++ include/notcurses/nckeys.h | 2 +- include/notcurses/notcurses.h | 11 ++++++----- src/lib/direct.cpp | 11 +++++++++++ src/lib/internal.h | 9 ++++++--- src/lib/notcurses.c | 22 +++++++++++++--------- src/lib/terminfo.c | 24 +++++++++++++++++++----- src/player/play.cpp | 10 +++++++++- 14 files changed, 115 insertions(+), 39 deletions(-) diff --git a/NEWS.md b/NEWS.md index afcf5eab2..48795cf88 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,11 +3,15 @@ rearrangements of Notcurses. * 2.2.3 (not yet released) * Add `SIGILL` to the set of fatal signals we handle. + * Added `NCKEY_SIGNAL`. `NCKEY_RESIZE` is now an alias for `NCKEY_SIGNAL`. + * `SIGCONT` now synthesizes a `NCKEY_SIGNAL`. * Add the `nctree` widget for line-oriented hierarchical data. See the new `notcurses_tree(3)` man page for complete information. * Implemented `NCBLIT_PIXEL` for terminals reporting Sixel support. - Attempt Sixel detection iff `NCOPTION_VERIFY_SIXEL` is provided to - `notcurses_init()`. Sixel detection can delay or even block initialization. + Added `notcurses_check_pixel_support()` and its companion + `ncdirect_check_pixel_support()`, which must be called (and must return + success) before `NCBLIT_PIXEL` will be available. `NCBLIT_PIXEL` degrades + to `NCBLIT_3x2` until support is verified. * 2.2.2 (2021-02-18): * `notcurses_stats()` no longer qualifies its `notcurses*` argument with diff --git a/USAGE.md b/USAGE.md index 46bb3665e..172c1b122 100644 --- a/USAGE.md +++ b/USAGE.md @@ -89,13 +89,14 @@ typedef enum { // prior to notcurses_init(), you should not set this bit. Even if you are // invoking setlocale(), this behavior shouldn't be an issue unless you're // doing something weird (setting a locale not based on LANG). -#define NCOPTION_INHIBIT_SETLOCALE 0x0001 +#define NCOPTION_INHIBIT_SETLOCALE 0x0001 -// Checking for Sixel support requires writing an escape, and then reading an -// inline reply from the terminal. Since this can interact poorly with actual -// user input, it's not done unless Sixel will actually be used. Set this flag -// to unconditionally test for Sixel support in notcurses_init(). -#define NCOPTION_VERIFY_SIXEL 0x0002 +// Checking for pixel support might require writing a control sequence, and +// then reading a reply directly from the terminal. If the terminal doesn't +// support this, the application will lock up. If you'll be using pixels, set +// this flag to perform the check in notcurses_init(). You must otherwise call +// notcurses_check_pixel() before NCBLIT_PIXEL will become available. +#define NCOPTION_VERIFY_PIXEL 0x0002ull // We typically install a signal handler for SIGWINCH that generates a resize // event in the notcurses_getc() queue. Set to inhibit this handler. @@ -306,6 +307,11 @@ bool notcurses_cansextants(const struct notcurses* nc); // Can we draw Braille? The Linux console cannot. bool notcurses_canbraille(const struct notcurses* nc); + +// If NCOPTION_VERIFY_PIXEL was not supplied to notcurses_init(), this +// function must successfully return before NCBLIT_PIXEL is available. Returns +// -1 on error, 0 if pixel mode is not supported, or 1 if it is supported. +int notcurses_check_pixel(struct notcurses* nc); ``` ## Direct mode diff --git a/doc/man/man3/notcurses_direct.3.md b/doc/man/man3/notcurses_direct.3.md index f9e4ec13f..f1589e6ca 100644 --- a/doc/man/man3/notcurses_direct.3.md +++ b/doc/man/man3/notcurses_direct.3.md @@ -72,6 +72,8 @@ notcurses_direct - minimal notcurses instances for styling text **bool ncdirect_canutf8(const struct ncdirect* ***n***);** +**int ncdirect_check_pixel_support(struct ncdirect* ***n***);** + **int ncdirect_hline_interp(struct ncdirect* ***n***, const char* ***egc***, int ***len***, uint64_t ***h1***, uint64_t ***h2***);** **int ncdirect_vline_interp(struct ncdirect* ***n***, const char* ***egc***, int ***len***, uint64_t ***h1***, uint64_t ***h2***);** @@ -144,6 +146,10 @@ information, consult **readline(3)**. If you want input echoed to the terminal while using **ncdirect_readline**, **NCDIRECT_OPTION_INHIBIT_CBREAK** must be supplied to **ncdirect_init**. +**ncdirect_check_pixel_support** must be called (and successfully return) +before **NCBLIT_PIXEL** can be used to render images; see +**notcurses_visual(3)** for more details. + # RETURN VALUES **ncdirect_init** returns **NULL** on failure. Otherwise, the return value @@ -153,6 +159,9 @@ to **ncdirect_stop**. **ncdirect_putstr** and **ncdirect_printf_aligned** return the number of bytes written on success. On failure, they return some negative number. +**ncdirect_check_pixel_support** returns -1 on error, 0 if there is no pixel +support, and 1 if pixel support is successfully detected. + All other functions return 0 on success, and non-zero on error. # SEE ALSO @@ -161,5 +170,6 @@ All other functions return 0 on success, and non-zero on error. **readline(3)** **notcurses(3)**, **notcurses_plane(3)**, +**notcurses_visual(3)**, **terminfo(5)**, **termios(3)** diff --git a/doc/man/man3/notcurses_init.3.md b/doc/man/man3/notcurses_init.3.md index a6654b9a4..bd479cf11 100644 --- a/doc/man/man3/notcurses_init.3.md +++ b/doc/man/man3/notcurses_init.3.md @@ -12,7 +12,6 @@ notcurses_init - initialize a notcurses instance ```c #define NCOPTION_INHIBIT_SETLOCALE 0x0001ull -#define NCOPTION_VERIFY_SIXEL 0x0002ull #define NCOPTION_NO_WINCH_SIGHANDLER 0x0004ull #define NCOPTION_NO_QUIT_SIGHANDLERS 0x0008ull #define NCOPTION_SUPPRESS_BANNERS 0x0020ull @@ -111,12 +110,6 @@ zero. The following flags are defined: the **LANG** environment variable. Your program should call **setlocale(3)** itself, usually as one of the first lines. -* **NCOPTION_VERIFY_SIXEL**: Checking for Sixel support requires writing an - escape, and then reading an inline reply from the terminal. Since this can - interact poorly with actual user input, it's not done unless Sixel will - actually be used. Set this flag to unconditionally test for Sixel support - in **notcurses_init**. - * **NCOPTION_NO_WINCH_SIGHANDLER**: A signal handler will usually be installed for **SIGWINCH**, resulting in **NCKEY_RESIZE** events being generated on input. With this flag, the handler will not be installed. diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md index 127f0f39d..424eef671 100644 --- a/doc/man/man3/notcurses_visual.3.md +++ b/doc/man/man3/notcurses_visual.3.md @@ -92,6 +92,8 @@ typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*); **int ncplane_qrcode(struct ncplane* ***n***, int* ***ymax***, int* ***xmax***, const void* ***data***, size_t ***len***)** +**int notcurses_check_pixel_support(struct notcurses* ***nc***);** + # DESCRIPTION An **ncvisual** is a virtual pixel framebuffer. They can be created from @@ -203,6 +205,16 @@ Finally, rendering operates slightly differently when two planes have both been blitted, and one lies atop the other. See **notcurses_render(3)** for more information. +# PIXEL BLITTING + +Some terminals support pixel-based output, either via Sixel or some bespoke +mechanism. Checking for Sixel support requires interrogating the terminal and +reading a response. This takes time, and will never complete if the terminal +doesn't respond. Before **NCBLIT_PIXEL** can be used, it is thus necessary to +check for support with **notcurses_check_pixel_support**. If this function has +not successfully returned, attempts to use **NCBLIT_PIXEL** will fall back to +**NCBLIT_3x2** (or fail, if **NCVISUAL_OPTION_NODEGRADE** is used). + # RETURN VALUES **ncvisual_from_file** returns an **ncvisual** object on success, or **NULL** diff --git a/include/ncpp/Direct.hh b/include/ncpp/Direct.hh index 819868013..a540f1ff3 100644 --- a/include/ncpp/Direct.hh +++ b/include/ncpp/Direct.hh @@ -217,6 +217,11 @@ namespace ncpp return ncdirect_canutf8 (direct); } + int check_pixel_support() noexcept + { + return ncdirect_check_pixel_support (direct); + } + private: ncdirect *direct; }; diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h index 2eaf046bc..c5373b0ab 100644 --- a/include/notcurses/direct.h +++ b/include/notcurses/direct.h @@ -216,6 +216,11 @@ API bool ncdirect_canopen_images(const struct ncdirect* n); // Is our encoding UTF-8? Requires LANG being set to a UTF8 locale. API bool ncdirect_canutf8(const struct ncdirect* n); +// This function must successfully return before NCBLIT_PIXEL is available. +// Returns -1 on error, 0 for no support, or 1 if pixel output is supported. +// Must not be called concurrently with either input or rasterization. +API int ncdirect_check_pixel_support(struct ncdirect* n); + // Draw horizontal/vertical lines using the specified channels, interpolating // between them as we go. The EGC may not use more than one column. For a // horizontal line, |len| cannot exceed the screen width minus the cursor's diff --git a/include/notcurses/nckeys.h b/include/notcurses/nckeys.h index e97e66d0f..a5c3bce2f 100644 --- a/include/notcurses/nckeys.h +++ b/include/notcurses/nckeys.h @@ -9,7 +9,7 @@ extern "C" { // Special composed key definitions. These values are added to 0x100000. #define NCKEY_INVALID suppuabize(0) -#define NCKEY_SIGNAL suppuabize(1) // generated internally in response to SIGWINCH / SIGCONT +#define NCKEY_SIGNAL suppuabize(1) // we received either SIGWINCH or SIGCONT #define NCKEY_UP suppuabize(2) #define NCKEY_RIGHT suppuabize(3) #define NCKEY_DOWN suppuabize(4) diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index a3992be3c..b394f9f4c 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -826,11 +826,7 @@ typedef enum { // doing something weird (setting a locale not based on LANG). #define NCOPTION_INHIBIT_SETLOCALE 0x0001ull -// Checking for Sixel support requires writing an escape, and then reading an -// inline reply from the terminal. Since this can interact poorly with actual -// user input, it's not done unless Sixel will actually be used. Set this flag -// to unconditionally test for Sixel support in notcurses_init(). -#define NCOPTION_VERIFY_SIXEL 0x0002ull +// NCOPTION_VERIFY_PIXEL was removed in 2.2.3. It ought be repurposed. FIXME. // We typically install a signal handler for SIGWINCH that generates a resize // event in the notcurses_getc() queue. Set to inhibit this handler. @@ -1237,6 +1233,11 @@ API bool notcurses_canbraille(const struct notcurses* nc); // Can we blit to pixel graphics? API bool notcurses_canpixel(const struct notcurses* nc); +// This function must successfully return before NCBLIT_PIXEL is available. +// Returns -1 on error, 0 for no support, or 1 if pixel output is supported. +// Must not be called concurrently with either input or rasterization. +API int notcurses_check_pixel_support(struct notcurses* nc); + typedef struct ncstats { // purely increasing stats uint64_t renders; // successful ncpile_render() runs diff --git a/src/lib/direct.cpp b/src/lib/direct.cpp index 9ae6a4de8..e4acd1ef3 100644 --- a/src/lib/direct.cpp +++ b/src/lib/direct.cpp @@ -637,6 +637,7 @@ ncdirect_stop_minimal(void* vnc){ ret |= close(nc->ctermfd); } ret |= ncdirect_flush(nc); + free_terminfo_cache(&nc->tcache); return ret; } @@ -1102,3 +1103,13 @@ int ncdirect_flush(const ncdirect* nc){ } return 0; } + +int ncdirect_check_pixel_support(ncdirect* n){ + if(query_term(&n->tcache, n->ctermfd)){ + return -1; + } + if(n->tcache.pixelon){ + return 1; + } + return 0; +} diff --git a/src/lib/internal.h b/src/lib/internal.h index 3a4acc4c7..9ce7f5669 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -286,11 +286,12 @@ typedef struct tinfo { // background_opaque is in use. detect this, and avoid the default if so. // bg_collides_default is either 0x0000000 or 0x1RRGGBB. uint32_t bg_collides_default; - bool sextants; // do we have (good, vetted) Unicode 13 sextant support? - bool braille; // do we have Braille support? (linux console does not) + pthread_mutex_t pixel_query; // only query for pixel support once char* pixelon; // enter pixel graphics mode char* pixeloff; // leave pixel graphics mode - bool sixel; // do we have Sixel support? + bool pixel_query_done; // have we yet performed pixel query? + bool sextants; // do we have (good, vetted) Unicode 13 sextant support? + bool braille; // do we have Braille support? (linux console does not) } tinfo; typedef struct ncinputlayer { @@ -386,6 +387,8 @@ int terminfostr(char** gseq, const char* name); // initialized. set |utf8| if we've verified UTF8 output encoding. int interrogate_terminfo(tinfo* ti, const char* termname, unsigned utf8); +void free_terminfo_cache(tinfo* ti); + // perform queries that require writing to the terminal, and reading a // response, rather than simply reading the terminfo database. can result // in a lengthy delay or even block if the terminal doesn't respond. diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index c831b0b88..4439f663d 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -777,14 +777,13 @@ init_banner(const notcurses* nc){ term_fg_palindex(nc, stdout, nc->tcache.colors <= 256 ? 50 % nc->tcache.colors : 0x20e080); printf("\n notcurses %s by nick black et al", notcurses_version()); term_fg_palindex(nc, stdout, nc->tcache.colors <= 256 ? 12 % nc->tcache.colors : 0x2080e0); - printf("\n %d rows %d cols (%sB) %zuB cells %d colors%s%s\n" + printf("\n %d rows %d cols (%sB) %zuB cells %d colors%s\n" " compiled with gcc-%s, %s-endian\n" " terminfo from %s\n", nc->stdplane->leny, nc->stdplane->lenx, bprefix(nc->stats.fbbytes, 1, prefixbuf, 0), sizeof(nccell), nc->tcache.colors, nc->tcache.RGBflag ? "+RGB" : "", - nc->tcache.sixel ? "+Sixel" : "", __VERSION__, #ifdef __BYTE_ORDER__ #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ @@ -908,6 +907,16 @@ recursive_lock_init(pthread_mutex_t *lock){ #endif } +int notcurses_check_pixel_support(notcurses* nc){ + if(query_term(&nc->tcache, nc->ttyfd)){ + return -1; + } + if(nc->tcache.pixelon){ + return 1; + } + return 0; +} + // FIXME cut this up into a few distinct pieces, yearrrgh notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){ notcurses_options defaultopts; @@ -1033,12 +1042,6 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){ goto err; } if(ret->ttyfd >= 0){ - if(opts->flags & NCOPTION_VERIFY_SIXEL){ - if(query_term(&ret->tcache, ret->ttyfd)){ - free_plane(ret->stdplane); - goto err; - } - } if(ret->tcache.smkx && tty_emit(ret->tcache.smkx, ret->ttyfd)){ free_plane(ret->stdplane); goto err; @@ -1215,6 +1218,7 @@ int notcurses_stop(notcurses* nc){ del_curterm(cur_term); ret |= pthread_mutex_destroy(&nc->statlock); ret |= pthread_mutex_destroy(&nc->pilelock); + free_terminfo_cache(&nc->tcache); free(nc); } return ret; @@ -2073,7 +2077,7 @@ bool notcurses_canchangecolor(const notcurses* nc){ } bool notcurses_canpixel(const notcurses* nc){ - return nc->tcache.sixel; + return nc->tcache.pixelon; } palette256* palette256_new(notcurses* nc){ diff --git a/src/lib/terminfo.c b/src/lib/terminfo.c index 7004df550..e2cae563d 100644 --- a/src/lib/terminfo.c +++ b/src/lib/terminfo.c @@ -71,6 +71,10 @@ apply_term_heuristics(tinfo* ti, const char* termname){ return 0; } +void free_terminfo_cache(tinfo* ti){ + pthread_mutex_destroy(&ti->pixel_query); +} + // termname is just the TERM environment variable. some details are not // exposed via terminfo, and we must make heuristic decisions based on // the detected terminal type, yuck :/. @@ -184,6 +188,8 @@ int interrogate_terminfo(tinfo* ti, const char* termname, unsigned utf8){ ti->fgop = "\x1b[39m"; ti->bgop = "\x1b[49m"; } + pthread_mutex_init(&ti->pixel_query, NULL); + ti->pixel_query_done = false; if(apply_term_heuristics(ti, termname)){ return -1; } @@ -229,8 +235,8 @@ query_sixel(tinfo* ti, int fd){ state = DONE; }else if(in == '4'){ if(!ti->pixelon){ - ti->pixelon = strdup("\ePq"); - ti->pixeloff = strdup("\e\\"); + ti->pixelon = "\ePq"; + ti->pixeloff = "\e\\"; } // FIXME else warning? } break; @@ -245,10 +251,18 @@ query_sixel(tinfo* ti, int fd){ return 0; } -// fd must be a real terminal, and must not be in nonblocking mode +// fd must be a real terminal, and must not be in nonblocking mode. uses the +// pthread_mutex_t of |ti| to only act once. int query_term(tinfo* ti, int fd){ - if(query_sixel(ti, fd)){ + if(fd < 0){ return -1; } - return 0; + int ret = 0; + pthread_mutex_lock(&ti->pixel_query); + if(!ti->pixel_query_done){ + ret = query_sixel(ti, fd); + ti->pixel_query_done = true; + } + pthread_mutex_unlock(&ti->pixel_query); + return ret; } diff --git a/src/player/play.cpp b/src/player/play.cpp index a9a6942d4..d18faa17b 100644 --- a/src/player/play.cpp +++ b/src/player/play.cpp @@ -143,6 +143,9 @@ auto perframe(struct ncvisual* ncv, struct ncvisual_options* vopts, }else if(keyp >= '0' && keyp <= '8'){ // FIXME eliminate ctrl/alt marsh->blitter = static_cast(keyp - '0'); vopts->blitter = marsh->blitter; + if(vopts->blitter == NCBLIT_PIXEL){ + notcurses_check_pixel_support(nc); + } continue; }else if(keyp == NCKey::Up){ // FIXME @@ -280,6 +283,9 @@ int direct_mode_player(int argc, char** argv, ncscale_e scalemode, ncblitter_e b return -1; } bool failed = false; + if(blitter == NCBLIT_PIXEL){ + dm.check_pixel_support(); + } { for(auto i = 0 ; i < argc ; ++i){ try{ @@ -303,7 +309,6 @@ auto main(int argc, char** argv) -> int { float timescale, displaytime; ncscale_e scalemode; notcurses_options ncopts{}; - ncopts.flags = NCOPTION_VERIFY_SIXEL; ncblitter_e blitter = NCBLIT_PIXEL; bool quiet = false; bool loop = false; @@ -348,6 +353,9 @@ auto main(int argc, char** argv) -> int { vopts.n = *stdn; vopts.scaling = scalemode; vopts.blitter = blitter; + if(vopts.blitter == NCBLIT_PIXEL){ + notcurses_check_pixel_support(nc); + } do{ struct marshal marsh = { .subtitle_plane = nullptr,