diff --git a/CMakeLists.txt b/CMakeLists.txt index 283ba795a..a3363fcdf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.13) -project(notcurses VERSION 0.9.0 +project(notcurses VERSION 1.0.0 DESCRIPTION "UI for modern terminal emulators" HOMEPAGE_URL "https://nick-black.com/dankwiki/index.php/notcurses" LANGUAGES C CXX) @@ -79,7 +79,7 @@ target_compile_definitions(notcurses-demo ) # tiny proofs of concept, one binary per source file -file(GLOB POCSRCS CONFIGURE_DEPENDS src/poc/*.c) +file(GLOB POCSRCS CONFIGURE_DEPENDS src/poc/*.c src/poc/*.cpp) foreach(f ${POCSRCS}) get_filename_component(fe "${f}" NAME_WE) add_executable(${fe} ${f}) @@ -202,11 +202,11 @@ file(GLOB MANPAGES1 CONFIGURE_DEPENDS doc/man/man1/*) file(GLOB MANPAGES3 CONFIGURE_DEPENDS doc/man/man3/*) install(FILES ${MANPAGES1} - DESTINATION ${CMAKE_INSTALL_PREFIX}/man/man1 + DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man1 ) install(FILES ${MANPAGES3} - DESTINATION ${CMAKE_INSTALL_PREFIX}/man/man3 + DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man3 ) install(TARGETS notcurses-demo DESTINATION bin) diff --git a/README.md b/README.md index 43f8a5ae2..9a0e01b7c 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ by [nick black](https://nick-black.com/dankwiki/index.php/Hack_on) (gcluster < 0x80; } +static inline int +cell_load_simple(struct ncplane* n, cell* c, char ch){ + cell_release(n, c); + c->channels &= ~CELL_WIDEASIAN_MASK; + c->gcluster = ch; + if(cell_simple_p(c)){ + return 1; + } + return -1; +} + // get the offset into the egcpool for this cell's EGC. returns meaningless and // unsafe results if called on a simple cell. static inline uint32_t @@ -1745,6 +1787,7 @@ acquired using the `notcurses_stats()` function. This function cannot fail. ```c typedef struct ncstats { uint64_t renders; // number of notcurses_render() runs + uint64_t failed_renders; // number of aborted renders, should be 0 uint64_t render_bytes; // bytes emitted to ttyfp uint64_t render_max_bytes; // max bytes emitted for a frame uint64_t render_min_bytes; // min bytes emitted for a frame @@ -1895,19 +1938,38 @@ These are pretty obvious, implementation-wise. * If your terminal has an option about default interpretation of "ambiguous-width characters" (this is actually a technical term from Unicode), ensure it is - set to **Wide**, not narrow. + set to **Wide**, not narrow. If that doesn't work, ensure it is set to + **Narrow**, heh. * If you can disable BiDi in your terminal, do so while running notcurses applications, until I have that handled better. notcurses doesn't recognize the BiDi state machine transitions, and thus merrily continues writing - left-to-right. ﷽! + left-to-right. Likewise, ultra-wide glyphs will have interesting effects. + ﷽! * The unit tests assume dimensions of at least 80x25. They might work in a smaller terminal. They might not. Don't file bugs on it. +### DirectColor detection + +notcurses aims to use only information found in the terminal's terminfo entry to detect capabilities, DirectColor +being one of them. Support for this is indicated by terminfo having a flag, added in NCURSES 6.1, named `RGB` set +to `true`. However, as of today there are few and far between terminfo entries which have the capability in their +database entry and so DirectColor won't be used in most cases. Terminal emulators have had for years a kludge to +work around this limitation of terminfo in the form of the `COLORTERM` environment variable which, if set to either +`truecolor` or `24bit` does the job of indicating the capability of sending the escapes 48 and 38 together with a +tripartite RGB (0 ≤ c ≤ 255 for all three components) to specify fore- and background colors. +Checking for `COLORTERM` admittedly goes against the goal stated at the top of this section but, for all practical +purposes, makes the detection work quite well **today**. + ### Fonts -Fonts end up being a whole thing. +Fonts end up being a whole thing, little of which is pleasant. I'll write this +up someday **FIXME**. + +### When all else fails... + +...fuck wit' it harder, hax0r. ## Supplemental material @@ -1930,9 +1992,13 @@ Fonts end up being a whole thing. * [tui-rs](https://github.com/fdehau/tui-rs) (Rust) * [blessed-contrib](https://github.com/yaronn/blessed-contrib) (Javascript) +* [FINAL CUT](https://github.com/gansm/finalcut) (C++) ### History +* 2019-12-18: notcurses [0.9.0 "You dig in! You dig out! You get out!"](https://github.com/dankamongmen/notcurses/releases/tag/v0.9.0), + and also the first contributor besides myself (@grendello). Last major + pre-GA release. * 2019-12-05: notcurses [0.4.0 "TRAP MUSIC ALL NIGHT LONG"](https://github.com/dankamongmen/notcurses/releases/tag/v0.4.0), the first generally usable notcurses. I prepare a [demo](https://www.youtube.com/watch?v=eEv2YRyiEVM), and release it on YouTube. @@ -1961,7 +2027,7 @@ Fonts end up being a whole thing. * Notcurses could never be what it is without decades of tireless, likely thankless work by Thomas E. Dickey on NCURSES. His FAQ is a model of engineering history. He exemplifies documentation excellence and - conservative, thoughtful stewardship. The Open Source community owes + conservative, thoughtful stewardship. The free software community owes Mr. Dickey a great debt. * Justine Tunney, one of my first friends at Google NYC, was always present with support, and pointed out the useful memstream functionality of diff --git a/debian/changelog b/debian/changelog index cca931c96..072e18089 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,15 @@ -notcurses (0.9.0-1) UNRELEASED; urgency=medium +notcurses (1.0.0-1) UNRELEASED; urgency=medium - * + * Install binaries' man pages in notcurses-bin + * Install library man pages in libnotcurses-dev - -- Nick Black Thu, 05 Dec 2019 04:37:32 -0500 + -- Nick Black Wed, 18 Dec 2019 06:47:44 -0500 + +notcurses (0.9.0-1) unstable; urgency=medium + + * New upstream version + + -- Nick Black Wed, 18 Dec 2019 05:32:55 -0500 notcurses (0.4.0-1) unstable; urgency=medium diff --git a/debian/libnotcurses-dev.install b/debian/libnotcurses-dev.install index a5c9897e3..464366604 100644 --- a/debian/libnotcurses-dev.install +++ b/debian/libnotcurses-dev.install @@ -2,3 +2,4 @@ usr/include usr/lib/*/*.so usr/lib/*/pkgconfig/*.pc usr/lib/*/cmake/* +usr/share/man/man3/* diff --git a/debian/notcurses-bin.install b/debian/notcurses-bin.install index 2eb548155..eb8ec7207 100644 --- a/debian/notcurses-bin.install +++ b/debian/notcurses-bin.install @@ -1,2 +1,3 @@ usr/bin/* usr/share/notcurses/* +usr/man/man1/* diff --git a/doc/man/man1/notcurses-demo.1 b/doc/man/man1/notcurses-demo.1 index b4cb87a72..545e0c9df 100644 --- a/doc/man/man1/notcurses-demo.1 +++ b/doc/man/man1/notcurses-demo.1 @@ -27,25 +27,25 @@ contains a set of text-based demonstrations of capabilities from the notcurses l .P (i)ntro—a setting of tone .P -(s)liders—a missing-piece puzzle made up of colorful blocks -.P -(u)niblocks—a series of blocks detailing Unicode pages -.P (m)axcolors—smoothly changing colors .P (l)uigi-a dashing plumber of Apennine persuasion .P +(u)niblocks—a series of blocks detailing Unicode pages +.P (b)oxes—pulsating boxes with a transparent center .P (g)rid—a gradient of color lain atop a great grid .P -(w)idecolors—letters of many languages in many colors +(s)liders—a missing-piece puzzle made up of colorful blocks .P -(v)iew—in which PNGs are rendered as text, and a video, too +(w)idechomper—a gremlin feasts upon wide characters +.P +(v)iew—images and a video are rendered as text .P (p)anelreels—demonstration of the panelreel high-level widget .P -(o)utro—a message of hope from the library author +(o)utro—a message of hope from the library's author .SH NOTES Proper display requires a terminal advertising the RGB terminfo(5) capability (necessary for specification of arbitrary 24bpp colors), and a monospaced font with good Unicode support. .SH SEE ALSO diff --git a/include/notcurses.h b/include/notcurses.h index f7aa0cdd3..ae30beda5 100644 --- a/include/notcurses.h +++ b/include/notcurses.h @@ -133,6 +133,9 @@ API int notcurses_stop(struct notcurses* nc); // successful call to notcurses_render(). API int notcurses_render(struct notcurses* nc); +// Return the topmost ncplane, of which there is always at least one. +API struct ncplane* notcurses_top(struct notcurses* n); + // All input is currently taken from stdin, though this will likely change. We // attempt to read a single UTF8-encoded Unicode codepoint, *not* an entire // Extended Grapheme Cluster. It is also possible that we will read a special @@ -279,7 +282,8 @@ API unsigned notcurses_supported_styles(const struct notcurses* nc); API int notcurses_palette_size(const struct notcurses* nc); typedef struct ncstats { - uint64_t renders; // number of notcurses_render() runs + uint64_t renders; // number of successful notcurses_render() runs + uint64_t failed_renders; // number of aborted renders, should be 0 uint64_t render_bytes; // bytes emitted to ttyfp int64_t render_max_bytes; // max bytes emitted for a frame int64_t render_min_bytes; // min bytes emitted for a frame @@ -309,12 +313,11 @@ API void notcurses_stats(const struct notcurses* nc, ncstats* stats); // of the resized ncplane. Finally, 'ylen' and 'xlen' are the dimensions of the // ncplane after resizing. 'ylen' must be greater than or equal to 'keepleny', // and 'xlen' must be greater than or equal to 'keeplenx'. It is an error to -// attempt to resize the standard plane. If either of 'keepy' or 'keepx' is -// non-zero, both must be non-zero. +// attempt to resize the standard plane. If either of 'keepleny' or 'keeplenx' +// is non-zero, both must be non-zero. // // Essentially, the kept material does not move. It serves to anchor the -// resized plane. If there is no kept material, the plane can move freely: -// it is possible to implement ncplane_move() in terms of ncplane_resize(). +// resized plane. If there is no kept material, the plane can move freely. API int ncplane_resize(struct ncplane* n, int keepy, int keepx, int keepleny, int keeplenx, int yoff, int xoff, int ylen, int xlen); @@ -342,16 +345,44 @@ API void ncplane_yx(const struct ncplane* n, int* RESTRICT y, int* RESTRICT x); API int ncplane_move_top(struct ncplane* n); API int ncplane_move_bottom(struct ncplane* n); -// Splice ncplane 'n' out of the z-buffer, and reinsert it below 'below'. -API int ncplane_move_below(struct ncplane* RESTRICT n, struct ncplane* RESTRICT below); - // Splice ncplane 'n' out of the z-buffer, and reinsert it above 'above'. -API int ncplane_move_above(struct ncplane* RESTRICT n, struct ncplane* RESTRICT above); +API int ncplane_move_above_unsafe(struct ncplane* RESTRICT n, + struct ncplane* RESTRICT above); + +static inline int +ncplane_move_above(struct ncplane* n, struct ncplane* above){ + if(n == above){ + return -1; + } + return ncplane_move_above_unsafe(n, above); +} + +// Splice ncplane 'n' out of the z-buffer, and reinsert it below 'below'. +API int ncplane_move_below_unsafe(struct ncplane* RESTRICT n, + struct ncplane* RESTRICT below); + +static inline int +ncplane_move_below(struct ncplane* n, struct ncplane* below){ + if(n == below){ + return -1; + } + return ncplane_move_below_unsafe(n, below); +} + +// Return the plane above this one, or NULL if this is at the top. +API struct ncplane* ncplane_below(struct ncplane* n); + +// Return the plane below this one, or NULL if this is at the bottom. +API struct ncplane* ncplane_below(struct ncplane* n); // Retrieve the cell at the cursor location on the specified plane, returning // it in 'c'. This copy is safe to use until the ncplane is destroyed/erased. API int ncplane_at_cursor(struct ncplane* n, cell* c); +// Retrieve the cell at the specified location on the specified plane, returning +// it in 'c'. This copy is safe to use until the ncplane is destroyed/erased. +API int ncplane_at_yx(struct ncplane* n, int y, int x, cell* c); + // Manipulate the opaque user pointer associated with this plane. // ncplane_set_userptr() returns the previous userptr after replacing // it with 'opaque'. the others simply return the userptr. @@ -1074,7 +1105,8 @@ API int ncplane_fadein(struct ncplane* n, const struct timespec* ts); // Working with cells #define CELL_TRIVIAL_INITIALIZER { .gcluster = '\0', .attrword = 0, .channels = 0, } -#define CELL_SIMPLE_INITIALIZER(c) { .gcluster = c, .attrword = 0, .channels = 0, } +#define CELL_SIMPLE_INITIALIZER(c) { .gcluster = (c), .attrword = 0, .channels = 0, } +#define CELL_INITIALIZER(c, a, chan) { .gcluster = (c), .attrword = (a), .channels = (chan), } static inline void cell_init(cell* c){ @@ -1174,6 +1206,17 @@ cell_simple_p(const cell* c){ return c->gcluster < 0x80; } +static inline int +cell_load_simple(struct ncplane* n, cell* c, char ch){ + cell_release(n, c); + c->channels &= ~CELL_WIDEASIAN_MASK; + c->gcluster = ch; + if(cell_simple_p(c)){ + return 1; + } + return -1; +} + // get the offset into the egcpool for this cell's EGC. returns meaningless and // unsafe results if called on a simple cell. static inline uint32_t @@ -1231,12 +1274,9 @@ ncplane_rounded_box(struct ncplane* n, uint32_t attr, uint64_t channels, if((ret = cells_rounded_box(n, attr, channels, &ul, &ur, &ll, &lr, &hl, &vl)) == 0){ ret = ncplane_box(n, &ul, &ur, &ll, &lr, &hl, &vl, ystop, xstop, ctlword); } - cell_release(n, &ul); - cell_release(n, &ur); - cell_release(n, &ll); - cell_release(n, &lr); - cell_release(n, &hl); - cell_release(n, &vl); + cell_release(n, &ul); cell_release(n, &ur); + cell_release(n, &ll); cell_release(n, &lr); + cell_release(n, &hl); cell_release(n, &vl); return ret; } @@ -1265,12 +1305,9 @@ ncplane_double_box(struct ncplane* n, uint32_t attr, uint64_t channels, if((ret = cells_double_box(n, attr, channels, &ul, &ur, &ll, &lr, &hl, &vl)) == 0){ ret = ncplane_box(n, &ul, &ur, &ll, &lr, &hl, &vl, ystop, xstop, ctlword); } - cell_release(n, &ul); - cell_release(n, &ur); - cell_release(n, &ll); - cell_release(n, &lr); - cell_release(n, &hl); - cell_release(n, &vl); + cell_release(n, &ul); cell_release(n, &ur); + cell_release(n, &ll); cell_release(n, &lr); + cell_release(n, &hl); cell_release(n, &vl); return ret; } diff --git a/src/demo/boxdemo.c b/src/demo/boxdemo.c index 985c9794d..a13a48083 100644 --- a/src/demo/boxdemo.c +++ b/src/demo/boxdemo.c @@ -49,28 +49,28 @@ int box_demo(struct notcurses* nc){ if(ncplane_putstr_aligned(n, ytargbase++, "┗━━┻━━┛", NCALIGN_CENTER) < 0){ return -1; } - ncplane_set_fg_rgb(n, 255, 255, 255); - ncplane_set_bg_rgb(n, 180, 40, 180); do{ int y = 0, x = 0; ncplane_dim_yx(n, &ylen, &xlen); while(ylen - y >= targy && xlen - x >= targx){ cell_set_fg_rgb(&ul, 107 - (y * 2), zbonus, 107 + (y * 2)); - cell_set_bg_rgb(&ul, zbonus, 20 + y, 20 + y); + cell_set_bg_rgb(&ul, 20, zbonus, 20); cell_set_fg_rgb(&ur, 107 - (y * 2), zbonus, 107 + (y * 2)); - cell_set_bg_rgb(&ur, zbonus, 20 + y, 20 + y); + cell_set_bg_rgb(&ur, 20, zbonus, 20); cell_set_fg_rgb(&hl, 107 - (y * 2), zbonus, 107 + (y * 2)); cell_set_bg_rgb(&hl, 20, zbonus, 20); cell_set_fg_rgb(&ll, 107 - (y * 2), zbonus, 107 + (y * 2)); - cell_set_bg_rgb(&ll, zbonus, 20 + y, 20 + y); + cell_set_bg_rgb(&ll, 20, zbonus, 20); cell_set_fg_rgb(&lr, 107 - (y * 2), zbonus, 107 + (y * 2)); - cell_set_bg_rgb(&lr, zbonus, 20 + y, 20 + y); - cell_set_fg_rgb(&vl, 20, zbonus, 20); - cell_set_bg_rgb(&vl, 107 - (y * 2), zbonus, 107 + (y * 2)); + cell_set_bg_rgb(&lr, 20, zbonus, 20); + cell_set_fg_rgb(&vl, 107 - (y * 2), zbonus, 107 + (y * 2)); + cell_set_bg_rgb(&vl, 20, zbonus, 20); if(ncplane_cursor_move_yx(n, y, x)){ return -1; } - if(ncplane_box_sized(n, &ul, &ur, &ll, &lr, &hl, &vl, ylen, xlen, 0)){ + if(ncplane_box_sized(n, &ul, &ur, &ll, &lr, &hl, &vl, ylen, xlen, + NCBOXGRAD_LEFT | NCBOXGRAD_BOTTOM | + NCBOXGRAD_RIGHT | NCBOXGRAD_TOP)){ return -1; } ylen -= 2; diff --git a/src/demo/demo.c b/src/demo/demo.c index 4a967bd4a..f868dc283 100644 --- a/src/demo/demo.c +++ b/src/demo/demo.c @@ -52,7 +52,7 @@ usage(const char* exe, int status){ fprintf(out, " s: run shuffle\n"); fprintf(out, " u: run uniblock\n"); fprintf(out, " v: run view\n"); - fprintf(out, " w: run widecolors\n"); + fprintf(out, " w: run widechomper\n"); exit(status); } @@ -63,8 +63,29 @@ intro(struct notcurses* nc){ return -1; } ncplane_erase(ncp); + if(ncplane_cursor_move_yx(ncp, 0, 0)){ + return -1; + } int x, y, rows, cols; ncplane_dim_yx(ncp, &rows, &cols); + cell ul = CELL_TRIVIAL_INITIALIZER, ur = CELL_TRIVIAL_INITIALIZER; + cell ll = CELL_TRIVIAL_INITIALIZER, lr = CELL_TRIVIAL_INITIALIZER; + cell hl = CELL_TRIVIAL_INITIALIZER, vl = CELL_TRIVIAL_INITIALIZER; + if(cells_rounded_box(ncp, CELL_STYLE_BOLD, 0, &ul, &ur, &ll, &lr, &hl, &vl)){ + return -1; + } + channels_set_fg_rgb(&ul.channels, 0xff, 0, 0); + channels_set_fg_rgb(&ur.channels, 0, 0xff, 0); + channels_set_fg_rgb(&ll.channels, 0, 0, 0xff); + channels_set_fg_rgb(&lr.channels, 0xff, 0xff, 0xff); + if(ncplane_box_sized(ncp, &ul, &ur, &ll, &lr, &hl, &vl, rows, cols, + NCBOXGRAD_TOP | NCBOXGRAD_BOTTOM | + NCBOXGRAD_RIGHT | NCBOXGRAD_LEFT)){ + return -1; + } + cell_release(ncp, &ul); cell_release(ncp, &ur); + cell_release(ncp, &ll); cell_release(ncp, &lr); + cell_release(ncp, &hl); cell_release(ncp, &vl); cell c; cell_init(&c); const char* cstr = "Δ"; @@ -135,7 +156,7 @@ ext_demos(struct notcurses* nc, const char* demos){ case 'g': ret = grid_demo(nc); break; case 'l': ret = luigi_demo(nc); break; case 'v': ret = view_demo(nc); break; - case 'w': ret = widecolor_demo(nc); break; + case 'w': ret = widechomper_demo(nc); break; case 'p': ret = panelreel_demo(nc); break; default: fprintf(stderr, "Unknown demo specification: %c\n", *demos); diff --git a/src/demo/demo.h b/src/demo/demo.h index 48b2c74e0..e1f1e87f5 100644 --- a/src/demo/demo.h +++ b/src/demo/demo.h @@ -12,7 +12,7 @@ extern "C" { extern struct timespec demodelay; int unicodeblocks_demo(struct notcurses* nc); -int widecolor_demo(struct notcurses* nc); +int widechomper_demo(struct notcurses* nc); int box_demo(struct notcurses* nc); int maxcolor_demo(struct notcurses* nc); int grid_demo(struct notcurses* nc); diff --git a/src/demo/grid.c b/src/demo/grid.c index 0e3f209e5..4979ea6dc 100644 --- a/src/demo/grid.c +++ b/src/demo/grid.c @@ -102,6 +102,9 @@ gridswitch_demo(struct notcurses* nc, struct ncplane *n){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&cl, 255 - rs * y, 255 - gs * (x + y), 255 - bs * x); cell_set_bg_rgb(&cl, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &cl); @@ -117,6 +120,9 @@ gridswitch_demo(struct notcurses* nc, struct ncplane *n){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&ll, 255 - rs * y, 255 - gs * (x + y), 255 - bs * x); cell_set_bg_rgb(&ll, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &ll); @@ -167,6 +173,9 @@ gridinv_demo(struct notcurses* nc, struct ncplane *n){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&cl, 0, 0, 0); cell_set_bg_rgb(&cl, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &cl); @@ -182,6 +191,9 @@ gridinv_demo(struct notcurses* nc, struct ncplane *n){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&ll, 0, 0, 0); cell_set_bg_rgb(&ll, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &ll); @@ -233,6 +245,9 @@ int grid_demo(struct notcurses* nc){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_bg_rgb(&cl, y, y, y); cell_set_bg_rgb(&cc, y, y, y); cell_set_bg_rgb(&cr, y, y, y); @@ -248,6 +263,9 @@ int grid_demo(struct notcurses* nc){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_bg_rgb(&ll, y, y, y); cell_set_bg_rgb(&lc, y, y, y); cell_set_bg_rgb(&lr, y, y, y); diff --git a/src/demo/luigi.c b/src/demo/luigi.c index 9fae18395..a7a37a33e 100644 --- a/src/demo/luigi.c +++ b/src/demo/luigi.c @@ -5,104 +5,109 @@ //2 = yellow //3 = green -static const char luigi1[] = "0000000000000000" -"0000000000000000" -"0000000111110000" -"0000011111120000" -"0000111111220000" -"0000111111111110" -"0000333223222000" -"0003223223322220" -"0003223322222222" -"0033223322232222" -"0033222223333330" -"0003322222333330" -"0000032222222200" -"0000311122200000" -"0003133313000000" -"0003133331300000" -"0033133333112200" -"0031133333332222" -"0031113333332222" -"0001113333333222" -"0001111333333222" -"0001111113331000" -"0001111111111000" -"0001111111113000" -"3333111111131100" -"3333111113311100" -"3333111131111000" -"3333111001111000" -"3333000003333000" -"3300000003333000" -"3000000003333330" -"0000000003333330"; +static const char* luigis[] = { + "0000000000000000" + "0000000000000000" + "0000000111110000" + "0000011111120000" + "0000111111220000" + "0000111111111110" + "0000333223222000" + "0003223223322220" + "0003223322222222" + "0033223322232222" + "0033222223333330" + "0003322222333330" + "0000032222222200" + "0000311122200000" + "0003133313000000" + "0003133331300000" + "0033133333112200" + "0031133333332222" + "0031113333332222" + "0001113333333222" + "0001111333333222" + "0001111113331000" + "0001111111111000" + "0001111111113000" + "3333111111131100" + "3333111113311100" + "3333111131111000" + "3333111001111000" + "3333000003333000" + "3300000003333000" + "3000000003333330" + "0000000003333330", -static const char luigi2[] = "0000000000000000" -"0000001111100000" -"0000111111200000" -"0001111112200000" -"0001111111111100" -"0003332232220000" -"0032232233222200" -"0032233222222220" -"0332233222322220" -"0332222233333300" -"0033222223333300" -"0003322222222000" -"0000111122000000" -"0003133113300000" -"0031333311300000" -"0031333311330000" -"0031333311130000" -"0031333332230000" -"0031333322220000" -"0011133322221000" -"0011133322221100" -"0011113322211100" -"0011111133111100" -"0001111133311000" -"0000111333333000" -"0000113333330000" -"0000011333300000" -"0000031113330000" -"0000033330330000" -"0000333330000000" -"0000333333300000" -"0000003333300000"; + "0000000000000000" + "0000001111100000" + "0000111111200000" + "0001111112200000" + "0001111111111100" + "0003332232220000" + "0032232233222200" + "0032233222222220" + "0332233222322220" + "0332222233333300" + "0033222223333300" + "0003322222222000" + "0000111122000000" + "0003133113300000" + "0031333311300000" + "0031333311330000" + "0031333311130000" + "0031333332230000" + "0031333322220000" + "0011133322221000" + "0011133322221100" + "0011113322211100" + "0011111133111100" + "0001111133311000" + "0000111333333000" + "0000113333330000" + "0000011333300000" + "0000031113330000" + "0000033330330000" + "0000333330000000" + "0000333333300000" + "0000003333300000", + + "0000001111100000" + "0000111111200000" + "0001111112200000" + "0001111111111100" + "0003332232220000" + "0032232233222200" + "0032233222222220" + "0332233222322220" + "0332222233333300" + "0333222223333300" + "0003322222222000" + "0000033322000000" + "0000111133100020" + "0003333113310222" + "0033333311313222" + "0333333311331222" + "0333333311331323" + "0333333111331330" + "3333331112132300" + "3333111111111000" + "2222211111111000" + "2222211111111003" + "2222111111111033" + "0222111111133333" + "0001311111133333" + "0031131111133333" + "3331113311133333" + "3333111100033333" + "3333310000000000" + "0333000000000000" + "0333000000000000" + "0033300000000000", + + NULL +}; -static const char luigi3[] = "0000001111100000" -"0000111111200000" -"0001111112200000" -"0001111111111100" -"0003332232220000" -"0032232233222200" -"0032233222222220" -"0332233222322220" -"0332222233333300" -"0333222223333300" -"0003322222222000" -"0000033322000000" -"0000111133100020" -"0003333113310222" -"0033333311313222" -"0333333311331222" -"0333333311331323" -"0333333111331330" -"3333331112132300" -"3333111111111000" -"2222211111111000" -"2222211111111003" -"2222111111111033" -"0222111111133333" -"0001311111133333" -"0031131111133333" -"3331113311133333" -"3333111100033333" -"3333310000000000" -"0333000000000000" -"0333000000000000" -"0033300000000000"; static int draw_luigi(struct ncplane* n, const char* sprite){ @@ -159,26 +164,25 @@ int luigi_demo(struct notcurses* nc){ int yoff = rows * 4 / 5 - height + 1; // tuned struct ncplane* lns[3]; int i; + struct ncplane* lastseen = NULL; for(i = 0 ; i < 3 ; ++i){ - lns[i] = notcurses_newplane(nc, height, 16, yoff, -16, NULL); + lns[i] = notcurses_newplane(nc, height, 16, yoff, 0, NULL); if(lns[i] == NULL){ while(--i){ ncplane_destroy(lns[i]); } return -1; } + lastseen = lns[i]; + draw_luigi(lns[i], luigis[i]); + ncplane_move_bottom(lastseen); // all start hidden underneath stdplane } - draw_luigi(lns[0], luigi1); - draw_luigi(lns[1], luigi2); - draw_luigi(lns[2], luigi3); - struct ncplane* lastseen = NULL; struct timespec stepdelay; ns_to_timespec(timespec_to_ns(&demodelay) / (cols - 16 - 1), &stepdelay); for(i = 0 ; i < cols - 16 - 1 ; ++i){ - if(lastseen){ // hide the previous sprite - ncplane_move_yx(lastseen, yoff, -16); - } + ncplane_move_bottom(lastseen); // hide the previous sprite lastseen = lns[i % 3]; + ncplane_move_top(lastseen); ncplane_move_yx(lastseen, yoff, i); notcurses_render(nc); nanosleep(&stepdelay, NULL); diff --git a/src/demo/view.c b/src/demo/view.c index 68532368b..869ae7ef5 100644 --- a/src/demo/view.c +++ b/src/demo/view.c @@ -22,7 +22,7 @@ view_video_demo(struct notcurses* nc){ ncplane_dim_yx(ncp, &dimy, &dimx); int averr; struct ncvisual* ncv; - ncv = ncplane_visual_open(ncp, "../tests/bob.mkv", &averr); + ncv = ncplane_visual_open(ncp, "../tests/fm6.mkv", &averr); if(!ncv){ return -1; } diff --git a/src/demo/widecolor.c b/src/demo/widechomper.c similarity index 97% rename from src/demo/widecolor.c rename to src/demo/widechomper.c index cda04a369..42636c971 100644 --- a/src/demo/widecolor.c +++ b/src/demo/widechomper.c @@ -7,6 +7,9 @@ #include #include "demo.h" +// Fill up the screen with as much crazy Unicode as we can, and then set a +// gremlin loose, looking to eat up all the wide characters. + // FIXME throw this in there somehow // ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ // ⎪⎢⎜│a²+b³ ⎟⎥⎪ @@ -283,6 +286,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, int bytes_out, int egs_out, int cols_out){ cell c = CELL_TRIVIAL_INITIALIZER; cell_load(n, &c, " "); + cell_set_fg_alpha(&c, CELL_ALPHA_TRANS); cell_set_bg_alpha(&c, CELL_ALPHA_TRANS); ncplane_set_default(n, &c); cell_release(n, &c); @@ -297,7 +301,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, } // bottom handle ncplane_cursor_move_yx(n, 4, 17); - ncplane_putegc(n, "┬", 0, 0, NULL); + ncplane_putegc(n, "┬", 0, channels, NULL); ncplane_cursor_move_yx(n, 5, 17); ncplane_putegc(n, "│", 0, channels, NULL); ncplane_cursor_move_yx(n, 6, 17); @@ -344,7 +348,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, } // Much of this text comes from http://kermitproject.org/utf8.html -int widecolor_demo(struct notcurses* nc){ +int widechomper_demo(struct notcurses* nc){ static const char* strs[] = { "Война и мир", "Бра́тья Карама́зовы", @@ -575,9 +579,8 @@ int widecolor_demo(struct notcurses* nc){ NULL }; const char** s; - int count = notcurses_palette_size(nc); - const int steps[] = { 1, 0x100, 0x40000, 0x10001, }; - const int starts[] = { 0x4000, 0x40, 0x10000, 0x400040, }; + const int steps[] = { 0x100, 0x100, 0x40000, 0x10001, }; + const int starts[] = { 0x004000, 0x000040, 0x010101, 0x400040, }; struct ncplane* n = notcurses_stdplane(nc); size_t i; @@ -588,13 +591,10 @@ int widecolor_demo(struct notcurses* nc){ cell c; struct timespec screenend; clock_gettime(CLOCK_MONOTONIC, &screenend); - ns_to_timespec(timespec_to_ns(&screenend) + timespec_to_ns(&demodelay), &screenend); + ns_to_timespec(timespec_to_ns(&screenend) + 2 * timespec_to_ns(&demodelay), &screenend); do{ // (re)draw a screen const int start = starts[i]; int step = steps[i]; - const int rollover = 256 / ((step & 0xff) | ((step & 0xff00) >> 8u) - | ((step & 0xff0000) >> 16u)); - int rollcount = 0; // number of times we've added this step cell_init(&c); int y, x, maxy, maxx; ncplane_dim_yx(n, &maxy, &maxx); @@ -634,11 +634,18 @@ int widecolor_demo(struct notcurses* nc){ } int ulen = 0; int r; - if((r = ncplane_putegc(n, &(*s)[idx], 0, channels, &ulen)) < 0){ - if(ulen < 0){ + if(wcwidth(wcs) <= maxx - x){ + if((r = ncplane_putegc(n, &(*s)[idx], 0, channels, &ulen)) < 0){ + if(ulen < 0){ + return -1; + } + } + }else{ + cell octo = CELL_INITIALIZER('#', 0, channels); + if((r = ncplane_putc(n, &octo)) < 1){ return -1; } - break; + cell_release(n, &octo); } ncplane_cursor_yx(n, &y, &x); idx += ulen; @@ -646,19 +653,7 @@ int widecolor_demo(struct notcurses* nc){ cols_out += r; ++egcs_out; } - if(++rollcount % rollover == 0){ - step *= 256; - } - if((unsigned)step >= 1ul << 24){ - step >>= 24u; - } - if(step == 0){ - step = 1; - } - if((rgb += step) >= count){ - rgb = 0; - step *= 256; - } + rgb += step; } }while(y < maxy && x < maxx); struct ncplane* mess = notcurses_newplane(nc, 7, 57, 1, 4, NULL); @@ -697,7 +692,7 @@ int widecolor_demo(struct notcurses* nc){ pthread_join(tid, NULL); ncplane_destroy(mess); if(key == NCKEY_RESIZE){ - notcurses_resize(nc, NULL, NULL); + notcurses_resize(nc, &maxy, &maxx); } }while(key == NCKEY_RESIZE); } diff --git a/src/lib/internal.h b/src/lib/internal.h index a99174ca3..e238aea8b 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -84,11 +84,15 @@ typedef struct notcurses { FILE* ttyfp; // FILE* for controlling tty, from opts->ttyfp FILE* ttyinfp; // FILE* for processing input unsigned char* damage; // damage map (row granularity) + char* mstream; // buffer for rendering memstream, see open_memstream(3) + FILE* mstreamfp;// FILE* for rendering memstream + size_t mstrsize;// size of rendering memstream int colors; // number of colors usable for this screen ncstats stats; // some statistics across the lifetime of the notcurses ctx // We verify that some terminfo capabilities exist. These needn't be checked // before further use; just use tiparm() directly. char* cup; // move cursor + bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor char* civis; // hide cursor // These might be NULL, and we can more or less work without them. Check! char* clearscr; // erase screen and home cursor @@ -112,9 +116,7 @@ typedef struct notcurses { char* italoff; // CELL_STYLE_ITALIC (disable) char* smkx; // enter keypad transmit mode (keypad_xmit) char* rmkx; // leave keypad transmit mode (keypad_local) - struct termios tpreserved; // terminal state upon entry - bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor bool CCCflag; // terminfo-reported "CCC" flag for palette set capability ncplane* top; // the contents of our topmost plane (initially entire screen) ncplane* stdscr;// aliases some plane from the z-buffer, covers screen @@ -168,21 +170,26 @@ flash_damage_map(unsigned char* damage, int count, bool val){ void ncplane_updamage(ncplane* n); // For our first attempt, O(1) uniform conversion from 8-bit r/g/b down to -// ~2.4-bit 6x6x6 ANSI cube + greyscale (assumed on entry; I know no way to +// ~2.4-bit 6x6x6 cube + greyscale (assumed on entry; I know no way to // even semi-portably recover the palette) proceeds via: map each 8-bit to // a 5-bit target grey. if all 3 components match, select that grey. // otherwise, c / 42.7 to map to 6 values. this never generates pure black // nor white, though, lame...FIXME static inline int -rgb_to_ansi256(unsigned r, unsigned g, unsigned b){ +rgb_quantize_256(unsigned r, unsigned g, unsigned b){ const unsigned GREYMASK = 0xf8; - r &= GREYMASK; - g &= GREYMASK; - b &= GREYMASK; - if(r == g && g == b){ // 5 MSBs match, return grey - r >>= 3u; - r += 232; - return r > 255 ? 255: r; + // if all 5 MSBs match, return grey from 24-member grey ramp or pure + // black/white from original 16 (0 and 15, respectively) + if((r & GREYMASK) == (g & GREYMASK) && (g & GREYMASK) == (b & GREYMASK)){ + // 256 / 26 == 9.846 + int gidx = r * 5 / 49 - 1; + if(gidx < 0){ + return 0; + } + if(gidx >= 24){ + return 15; + } + return 232 + gidx; } r /= 43; g /= 43; @@ -190,6 +197,31 @@ rgb_to_ansi256(unsigned r, unsigned g, unsigned b){ return r * 36 + g * 6 + b + 16; } +static inline int +term_emit(const char* name __attribute__ ((unused)), const char* seq, + FILE* out, bool flush){ + int ret = fprintf(out, "%s", seq); + if(ret < 0){ +// fprintf(stderr, "Error emitting %zub %s escape (%s)\n", strlen(seq), name, strerror(errno)); + return -1; + } + if((size_t)ret != strlen(seq)){ +// fprintf(stderr, "Short write (%db) for %zub %s sequence\n", ret, strlen(seq), name); + return -1; + } + if(flush && fflush(out)){ +// fprintf(stderr, "Error flushing after %db %s sequence (%s)\n", ret, name, strerror(errno)); + return -1; + } + return 0; +} + +static inline const char* +extended_gcluster(const ncplane* n, const cell* c){ + uint32_t idx = cell_egc_idx(c); + return n->pool.pool + idx; +} + #define NANOSECS_IN_SEC 1000000000 #ifdef __cplusplus diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index a7ec532f9..7678a9c64 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -24,8 +23,6 @@ #include "version.h" #include "egcpool.h" -#define ESC "\x1b" - // only one notcurses object can be the target of signal handlers, due to their // process-wide nature. static notcurses* _Atomic signal_nc = ATOMIC_VAR_INIT(NULL); // ugh @@ -159,29 +156,6 @@ int ncplane_putwstr_aligned(struct ncplane* n, int y, const wchar_t* gclustarr, return ncplane_putwstr(n, gclustarr); } -static inline uint64_t -timespec_to_ns(const struct timespec* t){ - return t->tv_sec * NANOSECS_IN_SEC + t->tv_nsec; -} - -static void -update_render_stats(const struct timespec* time1, const struct timespec* time0, - ncstats* stats){ - int64_t elapsed = timespec_to_ns(time1) - timespec_to_ns(time0); - //fprintf(stderr, "Rendering took %ld.%03lds\n", elapsed / NANOSECS_IN_SEC, - // (elapsed % NANOSECS_IN_SEC) / 1000000); - if(elapsed > 0){ // don't count clearly incorrect information, egads - ++stats->renders; - stats->render_ns += elapsed; - if(elapsed > stats->render_max_ns){ - stats->render_max_ns = elapsed; - } - if(elapsed < stats->render_min_ns){ - stats->render_min_ns = elapsed; - } - } -} - static const char NOTCURSES_VERSION[] = notcurses_VERSION_MAJOR "." notcurses_VERSION_MINOR "." @@ -229,6 +203,16 @@ int ncplane_at_cursor(ncplane* n, cell* c){ return cell_duplicate(n, c, &n->fb[fbcellidx(n, n->y, n->x)]); } +int ncplane_at_yx(ncplane* n, int y, int x, cell* c){ + if(y >= n->leny || x >= n->lenx){ + return true; + } + if(y < 0 || x < 0){ + return true; + } + return cell_duplicate(n, c, &n->fb[fbcellidx(n, y, x)]); +} + void ncplane_dim_yx(const ncplane* n, int* rows, int* cols){ if(rows){ *rows = n->leny; @@ -286,24 +270,6 @@ free_plane(ncplane* p){ } } -static int -term_emit(const char* name, const char* seq, FILE* out, bool flush){ - int ret = fprintf(out, "%s", seq); - if(ret < 0){ - fprintf(stderr, "Error emitting %zub %s escape (%s)\n", strlen(seq), name, strerror(errno)); - return -1; - } - if((size_t)ret != strlen(seq)){ - fprintf(stderr, "Short write (%db) for %zub %s sequence\n", ret, strlen(seq), name); - return -1; - } - if(flush && fflush(out)){ - fprintf(stderr, "Error flushing after %db %s sequence (%s)\n", ret, name, strerror(errno)); - return -1; - } - return 0; -} - // create a new ncplane at the specified location (relative to the true screen, // having origin at 0,0), having the specified size, and put it at the top of // the planestack. its cursor starts at its origin; its style starts as null. @@ -411,7 +377,12 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, if(fb == NULL){ return -1; } - ncplane_updamage(n); + // if we're the standard plane, the updamage can be charged at the end. it's + // unsafe to call now anyway, because if we shrank, the notcurses damage map + // has already been shrunk down + if(n != n->nc->stdscr){ + ncplane_updamage(n); // damage any lines we were on + } unsigned char* tmpdamage; if((tmpdamage = realloc(n->damage, sizeof(*n->damage) * ylen)) == NULL){ free(fb); @@ -440,7 +411,7 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, n->lenx = xlen; n->leny = ylen; free(preserved); - ncplane_updamage(n); + ncplane_updamage(n); // damage any lines we're now on return 0; } // we currently have maxy rows of maxx cells each. we will be keeping rows @@ -477,7 +448,7 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, n->lenx = xlen; n->leny = ylen; free(preserved); - ncplane_updamage(n); + ncplane_updamage(n); // damage any lines we're now on return 0; } @@ -522,10 +493,6 @@ int notcurses_resize(notcurses* n, int* rows, int* cols){ if((tmpdamage = malloc(sizeof(*n->damage) * *rows)) == NULL){ return -1; } - if(ncplane_resize_internal(n->stdscr, 0, 0, keepy, keepx, 0, 0, *rows, *cols)){ - free(tmpdamage); - return -1; - } if(oldcols < *cols){ // all are busted if rows got bigger free(n->damage); flash_damage_map(tmpdamage, *rows, true); @@ -535,6 +502,9 @@ int notcurses_resize(notcurses* n, int* rows, int* cols){ free(n->damage); } n->damage = tmpdamage; + if(ncplane_resize_internal(n->stdscr, 0, 0, keepy, keepx, 0, 0, *rows, *cols)){ + return -1; + } return 0; } @@ -575,6 +545,21 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){ char* longname_term = longname(); fprintf(stderr, "Term: %s\n", longname_term ? longname_term : "?"); nc->RGBflag = tigetflag("RGB") == 1; + if (nc->RGBflag == 0) { + // RGB terminfo capability being a new thing (as of ncurses 6.1), it's not commonly found in + // terminal entries today. COLORTERM, however, is a de-facto (if imperfect/kludgy) standard way + // of indicating DirectColor support for a terminal. The variable takes one of two case-sensitive + // values: + // + // truecolor + // 24bit + // + // https://gist.github.com/XVilka/8346728#true-color-detection gives some more information about + // the topic + // + const char* cterm = getenv("COLORTERM"); + nc->RGBflag = cterm && (strcmp(cterm, "truecolor") == 0 || strcmp(cterm, "24bit") == 0); + } nc->CCCflag = tigetflag("ccc") == 1; if((nc->colors = tigetnum("colors")) <= 0){ fprintf(stderr, "This terminal doesn't appear to support colors\n"); @@ -691,6 +676,8 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ ret->renderfp = opts->renderfp; ret->inputescapes = NULL; ret->ttyinfp = stdin; // FIXME + ret->mstream = NULL; + ret->mstrsize = 0; if(make_nonblocking(ret->ttyinfp)){ free(ret); return NULL; @@ -750,6 +737,11 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ free_plane(ret->top); goto err; } + if((ret->mstreamfp = open_memstream(&ret->mstream, &ret->mstrsize)) == NULL){ + free(ret->damage); + free_plane(ret->top); + goto err; + } flash_damage_map(ret->damage, ret->stdscr->leny, false); // term_emit("clear", ret->clear, ret->ttyfp, false); char prefixbuf[BPREFIXSTRLEN + 1]; @@ -808,8 +800,8 @@ int notcurses_stop(notcurses* nc){ ret |= tcsetattr(nc->ttyfd, TCSANOW, &nc->tpreserved); if(nc->stats.renders){ double avg = nc->stats.render_ns / (double)nc->stats.renders; - fprintf(stderr, "%ju renders, %.03gs total (%.03gs min, %.03gs max, %.02gs avg %.1f fps)\n", - nc->stats.renders, + fprintf(stderr, "%ju render%s, %.03gs total (%.03gs min, %.03gs max, %.02gs avg %.1f fps)\n", + nc->stats.renders, nc->stats.renders == 1 ? "" : "s", nc->stats.render_ns / 1000000000.0, nc->stats.render_min_ns / 1000000000.0, nc->stats.render_max_ns / 1000000000.0, @@ -821,6 +813,8 @@ int notcurses_stop(notcurses* nc){ nc->stats.render_max_bytes / 1024.0, avg / 1024); } + fprintf(stderr, "%ju failed render%s\n", nc->stats.failed_renders, + nc->stats.failed_renders == 1 ? "" : "s"); fprintf(stderr, "Emits/elides: def %lu/%lu fg %lu/%lu bg %lu/%lu\n", nc->stats.defaultemissions, nc->stats.defaultelisions, @@ -835,7 +829,7 @@ int notcurses_stop(notcurses* nc){ (nc->stats.fgelisions * 100.0) / (nc->stats.fgemissions + nc->stats.fgelisions), (nc->stats.bgemissions + nc->stats.bgelisions) == 0 ? 0 : (nc->stats.bgelisions * 100.0) / (nc->stats.bgemissions + nc->stats.bgelisions)); - fprintf(stderr, "Cells emitted; %ju elided: %ju (%.2f%%)\n", + fprintf(stderr, "Cells emitted: %ju elided: %ju (%.2f%%)\n", nc->stats.cellemissions, nc->stats.cellelisions, (nc->stats.cellemissions + nc->stats.cellelisions) == 0 ? 0 : (nc->stats.cellelisions * 100.0) / (nc->stats.cellemissions + nc->stats.cellelisions)); @@ -844,6 +838,11 @@ int notcurses_stop(notcurses* nc){ nc->top = p->z; free_plane(p); } + free(nc->damage); + if(nc->mstreamfp){ + fclose(nc->mstreamfp); + } + free(nc->mstream); input_free_esctrie(&nc->inputescapes); ret |= pthread_mutex_destroy(&nc->lock); free(nc); @@ -892,130 +891,22 @@ int ncplane_set_bg_alpha(ncplane *n, int alpha){ } int ncplane_set_default(ncplane* ncp, const cell* c){ - return cell_duplicate(ncp, &ncp->defcell, c); + int ret = cell_duplicate(ncp, &ncp->defcell, c); + if(ret < 0){ + return -1; + } + ncplane_updamage(ncp); + return ret; } int ncplane_default(ncplane* ncp, cell* c){ return cell_duplicate(ncp, c, &ncp->defcell); } -// 3 for foreground, 4 for background, ugh FIXME -static int -term_esc_rgb(FILE* out, int esc, unsigned r, unsigned g, unsigned b){ - #define RGBESC1 ESC "[" - #define RGBESC2 "8;2;" - // rrr;ggg;bbbm - char rgbesc[] = RGBESC1 " " RGBESC2 " "; - int len = strlen(RGBESC1); - rgbesc[len++] = esc + '0'; - len += strlen(RGBESC2); - if(r > 99){ rgbesc[len++] = r / 100 + '0'; } - if(r > 9){ rgbesc[len++] = (r % 100) / 10 + '0'; } - rgbesc[len++] = (r % 10) + '0'; - rgbesc[len++] = ';'; - if(g > 99){ rgbesc[len++] = g / 100 + '0'; } - if(g > 9){ rgbesc[len++] = (g % 100) / 10 + '0'; } - rgbesc[len++] = g % 10 + '0'; - rgbesc[len++] = ';'; - if(b > 99){ rgbesc[len++] = b / 100 + '0'; } - if(b > 9){ rgbesc[len++] = (b % 100) / 10 + '0'; } - rgbesc[len++] = b % 10 + '0'; - rgbesc[len++] = 'm'; - rgbesc[len] = '\0'; - int w; - if((w = fprintf(out, "%.*s", len, rgbesc)) < len){ - return -1; - } - return 0; -} - -static int -term_bg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ - // We typically want to use tputs() and tiperm() to acquire and write the - // escapes, as these take into account terminal-specific delays, padding, - // etc. For the case of DirectColor, there is no suitable terminfo entry, but - // we're also in that case working with hopefully more robust terminals. - // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. - if(nc->RGBflag){ - return term_esc_rgb(out, 4, r, g, b); - }else{ - if(nc->setab == NULL){ - return -1; - } - // For 256-color indexed mode, start constructing a palette based off - // the inputs *if we can change the palette*. If more than 256 are used on - // a single screen, start... combining close ones? For 8-color mode, simple - // interpolation. I have no idea what to do for 88 colors. FIXME - if(nc->colors >= 256){ - term_emit("setab", tiparm(nc->setab, rgb_to_ansi256(r, g, b)), out, false); - } - return -1; - } - return 0; -} - -static int -term_fg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ - // We typically want to use tputs() and tiperm() to acquire and write the - // escapes, as these take into account terminal-specific delays, padding, - // etc. For the case of DirectColor, there is no suitable terminfo entry, but - // we're also in that case working with hopefully more robust terminals. - // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. - if(nc->RGBflag){ - return term_esc_rgb(out, 3, r, g, b); - }else{ - if(nc->setaf == NULL){ - return -1; - } - if(nc->colors >= 256){ - term_emit("setaf", tiparm(nc->setaf, rgb_to_ansi256(r, g, b)), out, false); - } - // For 256-color indexed mode, start constructing a palette based off - // the inputs *if we can change the palette*. If more than 256 are used on - // a single screen, start... combining close ones? For 8-color mode, simple - // interpolation. I have no idea what to do for 88 colors. FIXME - return -1; - } - return 0; -} - -static inline const char* -extended_gcluster(const ncplane* n, const cell* c){ - uint32_t idx = cell_egc_idx(c); - return n->pool.pool + idx; -} - const char* cell_extended_gcluster(const struct ncplane* n, const cell* c){ return extended_gcluster(n, c); } -// write the cell's UTF-8 grapheme cluster to the provided FILE*. returns the -// number of columns occupied by this EGC (only an approximation; it's actually -// a property of the font being used). -static int -term_putc(FILE* out, const ncplane* n, const cell* c){ - if(cell_simple_p(c)){ - if(c->gcluster == 0 || iscntrl(c->gcluster)){ -// fprintf(stderr, "[ ]\n"); - if(fputc(' ', out) == EOF){ - return -1; - } - }else{ -// fprintf(stderr, "[%c]\n", c->gcluster); - if(fputc(c->gcluster, out) == EOF){ - return -1; - } - } - }else{ - const char* ext = extended_gcluster(n, c); -// fprintf(stderr, "[%s]\n", ext); - if(fprintf(out, "%s", ext) < 0){ // FIXME check for short write? - return -1; - } - } - return 0; -} - static void advance_cursor(ncplane* n, int cols){ if(cursor_invalid_p(n)){ @@ -1023,158 +914,12 @@ advance_cursor(ncplane* n, int cols){ } if((n->x += cols) >= n->lenx){ ++n->y; - n->x -= n->lenx; + n->x = 0; } } -// check the current and target style bitmasks against the specified 'stylebit'. -// if they are different, and we have the necessary capability, write the -// applicable terminfo entry to 'out'. returns -1 only on a true error. -static int -term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit, - const char* ton, const char* toff){ - int ret = 0; - unsigned curon = cur & stylebit; - unsigned targon = targ & stylebit; - if(curon != targon){ - if(targon){ - if(ton){ - ret = term_emit("ton", ton, out, false); - } - }else{ - if(toff){ // how did this happen? we can turn it on, but not off? - ret = term_emit("toff", toff, out, false); - } - } - } - if(ret < 0){ - return -1; - } - return 0; -} - -// write any escape sequences necessary to set the desired style -static int -term_setstyles(const notcurses* nc, FILE* out, uint32_t* curattr, const cell* c, - bool* normalized){ - *normalized = false; - uint32_t cellattr = cell_styles(c); - if(cellattr == *curattr){ - return 0; // happy agreement, change nothing - } - int ret = 0; - // if only italics changed, don't emit any sgr escapes. xor of current and - // target ought have all 0s in the lower 8 bits if only italics changed. - if((cellattr ^ *curattr) & 0x00ff0000ul){ - *normalized = true; // FIXME this is pretty conservative - // if everything's 0, emit the shorter sgr0 - if(nc->sgr0 && ((cellattr & CELL_STYLE_MASK) == 0)){ - if(term_emit("sgr0", nc->sgr0, out, false) < 0){ - ret = -1; - } - }else if(term_emit("sgr", tiparm(nc->sgr, cellattr & CELL_STYLE_STANDOUT, - cellattr & CELL_STYLE_UNDERLINE, - cellattr & CELL_STYLE_REVERSE, - cellattr & CELL_STYLE_BLINK, - cellattr & CELL_STYLE_DIM, - cellattr & CELL_STYLE_BOLD, - cellattr & CELL_STYLE_INVIS, - cellattr & CELL_STYLE_PROTECT, 0), - out, false) < 0){ - ret = -1; - } - } - // sgr will blow away italics if they were set beforehand - ret |= term_setstyle(out, *curattr, cellattr, CELL_STYLE_ITALIC, nc->italics, nc->italoff); - *curattr = cellattr; - return ret; -} - -// Find the topmost cell for this coordinate by walking down the z-buffer, -// looking for an intersecting ncplane. Once we've found one, check it for -// transparency in either the back- or foreground. If the alpha channel is -// active, keep descending and blending until we hit opacity, or bedrock. We -// recurse to find opacity, and blend the result into what we have. The -// 'findfore' and 'findback' bools control our recursion--there's no point in -// going further down when a color is locked in, so don't (for instance) recurse -// further when we have a transparent foreground and opaque background atop an -// opaque foreground and transparent background. The cell we ultimately return -// (a const ref to 'c') is backed by '*retp' via rawdog copy; the caller must -// not call cell_release() upon it, nor use it beyond the scope of the render. -// -// So, as we go down, we find planes which can have impact on the result. Once -// we've locked the result in (base case), write the deep values we have to 'c'. -// Then, as we come back up, blend them as appropriate. The actual glyph is -// whichever one occurs at the top with a non-transparent α (α < 3). To effect -// tail recursion, though, we instead write first, and then recurse, blending -// as we descend. α <= 0 is opaque. α >= 3 is fully transparent. -static ncplane* -dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, - bool* damage){ - while(p){ - // where in the plane this coordinate would be, based off absy/absx. the - // true origin is 0,0, so abs=2,2 means coordinate 3,3 would be 1,1, while - // abs=-2,-2 would make coordinate 3,3 relative 5, 5. - int poffx, poffy; - poffy = y - p->absy; - poffx = x - p->absx; - if(poffy < p->leny && poffy >= 0){ - if(poffx < p->lenx && poffx >= 0){ // p is valid for this y, x - const cell* vis = &p->fb[fbcellidx(p, poffy, poffx)]; - // if we never loaded any content into the cell (or obliterated it by - // writing in a zero), use the plane's background cell. - if(vis->gcluster == 0){ - vis = &p->defcell; - } - bool lockedglyph = false; - int nalpha; - if(falpha > 0 && (nalpha = cell_get_fg_alpha(vis)) < CELL_ALPHA_TRANS){ - if(c->gcluster == 0){ // never write fully trans glyphs, never replace - if( (c->gcluster = vis->gcluster) ){ // index copy only - lockedglyph = true; // must return this ncplane for this glyph - c->attrword = vis->attrword; - cell_set_fchannel(c, cell_get_fchannel(vis)); // FIXME blend it in - falpha -= (CELL_ALPHA_TRANS - nalpha); // FIXME blend it in - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } - } - } - } - if(balpha > 0 && (nalpha = cell_get_bg_alpha(vis)) < CELL_ALPHA_TRANS){ - cell_set_bchannel(c, cell_get_bchannel(vis)); // FIXME blend it in - balpha -= (CELL_ALPHA_TRANS - nalpha); - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } - } - if((falpha > 0 || balpha > 0) && p->z){ // we must go further! - ncplane* cand = dig_visible_cell(c, y, x, p->z, falpha, balpha, damage); - if(!lockedglyph && cand){ - p = cand; - } - } - return p; - } - } - p = p->z; - } - // should never happen for valid y, x thanks to the stdplane. you fucked up! - return NULL; -} - -static inline ncplane* -visible_cell(cell* c, int y, int x, ncplane* n, bool* damage){ - cell_init(c); - return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANS, CELL_ALPHA_TRANS, damage); -} - -// Call with c->gcluster == 3, falpha == 3, balpha == 0, *retp == topplane. - // 'n' ends up above 'above' -int ncplane_move_above(ncplane* restrict n, ncplane* restrict above){ +int ncplane_move_above_unsafe(ncplane* restrict n, ncplane* restrict above){ ncplane** an = find_above_ncplane(n); if(an == NULL){ return -1; @@ -1186,11 +931,12 @@ int ncplane_move_above(ncplane* restrict n, ncplane* restrict above){ *an = n->z; // splice n out n->z = above; // attach above below n *aa = n; // spline n in above + ncplane_updamage(n); // conservative (we might not actually be visible) return 0; } // 'n' ends up below 'below' -int ncplane_move_below(ncplane* restrict n, ncplane* restrict below){ +int ncplane_move_below_unsafe(ncplane* restrict n, ncplane* restrict below){ ncplane** an = find_above_ncplane(n); if(an == NULL){ return -1; @@ -1198,6 +944,7 @@ int ncplane_move_below(ncplane* restrict n, ncplane* restrict below){ *an = n->z; // splice n out n->z = below->z; // reattach subbelow list to n below->z = n; // splice n in below + ncplane_updamage(n); // conservative (we might not actually be visible) return 0; } @@ -1209,6 +956,7 @@ int ncplane_move_top(ncplane* n){ *an = n->z; // splice n out n->z = n->nc->top; n->nc->top = n; + ncplane_updamage(n); return 0; } @@ -1224,217 +972,10 @@ int ncplane_move_bottom(ncplane* n){ } *an = n; n->z = NULL; + ncplane_updamage(n); return 0; } -static int -blocking_write(int fd, const char* buf, size_t buflen){ - size_t written = 0; - do{ - ssize_t w = write(fd, buf + written, buflen - written); - if(w < 0){ - if(errno != EAGAIN && errno != EWOULDBLOCK){ - return -1; - } - }else{ - written += w; - } - if(written < buflen){ - struct pollfd pfd = { - .fd = fd, - .events = POLLOUT, - .revents = 0, - }; - poll(&pfd, 1, -1); - } - }while(written < buflen); - return 0; -} - -// determine the best palette for the current frame, and write the necessary -// escape sequences to 'out'. for now, we just assume the ANSI palettes. at -// 256 colors, this is the 16 normal ones, 6x6x6 color cubes, and 32 greys. -// it's probably better to sample the darker regions rather than cover so much -// chroma, but whatever....FIXME -static int -prep_optimized_palette(notcurses* nc, FILE* out __attribute__ ((unused))){ - if(nc->RGBflag){ - return 0; // DirectColor, no need to write palette - } - if(!nc->CCCflag){ - return 0; // can't change palette - } - // FIXME - return 0; -} - -// FIXME this needs to keep an invalidation bitmap, rather than blitting the -// world every time -static inline int -notcurses_render_internal(notcurses* nc){ - int ret = 0; - int y, x; - char* buf = NULL; - size_t buflen = 0; - FILE* out = open_memstream(&buf, &buflen); // worth keeping around? - if(out == NULL){ - return -1; - } - // no need to write a clearscreen, since we update everything that's been - // changed. we explicitly move the cursor at the beginning of each line - // (to work around broken prior lines), so no need to home it expliticly. - prep_optimized_palette(nc, out); // FIXME do what on failure? - uint32_t curattr = 0; // current attributes set (does not include colors) - // FIXME as of at least gcc 9.2.1, we get a false -Wmaybe-uninitialized below - // when using these without explicit initializations. for the life of me, i - // can't see any such path, and valgrind is cool with it, so what ya gonna do? - unsigned lastr = 0, lastg = 0, lastb = 0; - unsigned lastbr = 0, lastbg = 0, lastbb = 0; - // we can elide a color escape iff the color has not changed between the two - // cells and the current cell uses no defaults, or if both the current and - // the last used both defaults. - bool fgelidable = false, bgelidable = false, defaultelidable = false; - for(y = 0 ; y < nc->stdscr->leny ; ++y){ - bool linedamaged = false; // have we repositioned the cursor to start line? - bool newdamage = nc->damage[y]; -// fprintf(stderr, "nc->damage[%d] (%p) = %u\n", y, nc->damage + y, nc->damage[y]); - if(newdamage){ - nc->damage[y] = 0; - } - // move to the beginning of the line, in case our accounting was befouled - // by wider- (or narrower-) than-reported characters - for(x = 0 ; x < nc->stdscr->lenx ; ++x){ - unsigned r, g, b, br, bg, bb; - ncplane* p; - cell c; // no need to initialize - p = visible_cell(&c, y, x, nc->top, &newdamage); - assert(p); - // don't try to print a wide character on the last column; it'll instead - // be printed on the next line. they probably shouldn't be admitted, but - // we can end up with one due to a resize. - if((x + 1 >= nc->stdscr->lenx && cell_double_wide_p(&c))){ - continue; - } - if(!linedamaged){ - if(newdamage){ - term_emit("cup", tiparm(nc->cup, y, x), out, false); - nc->stats.cellelisions += x; - nc->stats.cellemissions += (nc->stdscr->lenx - x); - linedamaged = true; - }else{ - continue; - } - } - // set the style. this can change the color back to the default; if it - // does, we need update our elision possibilities. - bool normalized; - term_setstyles(nc, out, &curattr, &c, &normalized); - if(normalized){ - defaultelidable = true; - bgelidable = false; - fgelidable = false; - } - // we allow these to be set distinctly, but terminfo only supports using - // them both via the 'op' capability. unless we want to generate the 'op' - // escapes ourselves, if either is set to default, we first send op, and - // then a turnon for whichever aren't default. - - // we can elide the default set iff the previous used both defaults - if(cell_fg_default_p(&c) || cell_bg_default_p(&c)){ - if(!defaultelidable){ - ++nc->stats.defaultemissions; - term_emit("op", nc->op, out, false); - }else{ - ++nc->stats.defaultelisions; - } - // if either is not default, this will get turned off - defaultelidable = true; - fgelidable = false; - bgelidable = false; - } - - // we can elide the foreground set iff the previous used fg and matched - if(!cell_fg_default_p(&c)){ - cell_get_fg_rgb(&c, &r, &g, &b); - if(fgelidable && lastr == r && lastg == g && lastb == b){ - ++nc->stats.fgelisions; - }else{ - term_fg_rgb8(nc, out, r, g, b); - ++nc->stats.fgemissions; - fgelidable = true; - } - lastr = r; lastg = g; lastb = b; - defaultelidable = false; - } - if(!cell_bg_default_p(&c)){ - cell_get_bg_rgb(&c, &br, &bg, &bb); - if(bgelidable && lastbr == br && lastbg == bg && lastbb == bb){ - ++nc->stats.bgelisions; - }else{ - term_bg_rgb8(nc, out, br, bg, bb); - ++nc->stats.bgemissions; - bgelidable = true; - } - lastbr = br; lastbg = bg; lastbb = bb; - defaultelidable = false; - } -// fprintf(stderr, "[%02d/%02d] 0x%02x 0x%02x 0x%02x %p\n", y, x, r, g, b, p); - term_putc(out, p, &c); - if(cell_double_wide_p(&c)){ - ++x; - } - } - if(linedamaged == false){ - nc->stats.cellelisions += x; - } - } - ret |= fclose(out); - fflush(nc->ttyfp); - if(blocking_write(nc->ttyfd, buf, buflen)){ - ret = -1; - } -/*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions, - fgelisions, fgemissions, bgelisions, bgemissions);*/ - if(nc->renderfp){ - fprintf(nc->renderfp, "%s\n", buf); - } - free(buf); - return buflen; -} - -static void -mutex_unlock(void* vlock){ - pthread_mutex_unlock(vlock); -} - -int notcurses_render(notcurses* nc){ - int ret; - struct timespec start, done; - clock_gettime(CLOCK_MONOTONIC_RAW, &start); - pthread_mutex_lock(&nc->lock); - pthread_cleanup_push(mutex_unlock, &nc->lock); - ret = 0; - int bytes = notcurses_render_internal(nc); - int dimy, dimx; - notcurses_resize(nc, &dimy, &dimx); - if(bytes > 0){ - nc->stats.render_bytes += bytes; - if(bytes > nc->stats.render_max_bytes){ - nc->stats.render_max_bytes = bytes; - } - if(bytes < nc->stats.render_min_bytes){ - nc->stats.render_min_bytes = bytes; - } - } - clock_gettime(CLOCK_MONOTONIC_RAW, &done); - update_render_stats(&done, &start, &nc->stats); - if(bytes < 0){ - ret = -1; - } - pthread_cleanup_pop(1); - return ret; -} - int ncplane_cursor_move_yx(ncplane* n, int y, int x){ if(x >= n->lenx || x < 0){ return -1; @@ -1479,18 +1020,54 @@ int cell_duplicate(ncplane* n, cell* targ, const cell* c){ return ulen; } +static inline void +cell_set_wide(cell* c){ + c->channels |= CELL_WIDEASIAN_MASK; +} + +static inline void +cell_obliterate(ncplane* n, cell* c){ + cell_release(n, c); + cell_init(c); +} + int ncplane_putc(ncplane* n, const cell* c){ ncplane_lock(n); if(cursor_invalid_p(n)){ ncplane_unlock(n); return -1; } + bool wide = cell_double_wide_p(c); + // A wide character obliterates anything to its immediate right (and marks + // that cell as wide). Any character placed atop one half of a wide character + // obliterates the other half. Note that a wide char can thus obliterate two + // wide chars, totalling four columns. cell* targ = &n->fb[fbcellidx(n, n->y, n->x)]; + if(n->x > 0){ + if(cell_double_wide_p(targ)){ // replaced cell is half of a wide char + if(targ->gcluster == 0){ // we're the right half + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x - 1)]); + }else{ + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x + 1)]); + } + } + } if(cell_duplicate(n, targ, c) < 0){ ncplane_unlock(n); return -1; } - int cols = 1 + cell_double_wide_p(targ); + int cols = 1 + wide; + if(wide){ // must set our right wide, and check for further damage + if(n->x < n->lenx - 1){ // check to our right + cell* candidate = &n->fb[fbcellidx(n, n->y, n->x + 1)]; + if(n->x < n->lenx - 2){ + if(cell_double_wide_p(candidate) && targ->gcluster){ // left half + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x + 2)]); + } + } + cell_set_wide(candidate); + } + } n->damage[n->y] = true; advance_cursor(n, cols); ncplane_unlock(n); @@ -1498,11 +1075,7 @@ int ncplane_putc(ncplane* n, const cell* c){ } int ncplane_putsimple(struct ncplane* n, char c){ - cell ce = { - .gcluster = c, - .attrword = ncplane_get_attr(n), - .channels = ncplane_get_channels(n), - }; + cell ce = CELL_INITIALIZER(c, ncplane_get_attr(n), ncplane_get_channels(n)); if(!cell_simple_p(&ce)){ return -1; } @@ -1552,6 +1125,7 @@ int cell_load(ncplane* n, cell* c, const char* gcluster){ int bytes; int cols; if((bytes = utf8_egc_len(gcluster, &cols)) >= 0 && bytes <= 1){ + c->channels &= ~CELL_WIDEASIAN_MASK; c->gcluster = *gcluster; return !!c->gcluster; } @@ -1668,28 +1242,33 @@ int ncplane_vprintf(ncplane* n, const char* format, va_list ap){ int ncplane_hline_interp(ncplane* n, const cell* c, int len, uint64_t c1, uint64_t c2){ - unsigned r1, g1, b1, r2, g2, b2; - unsigned br1, bg1, bb1, br2, bg2, bb2; - channels_get_fg_rgb(c1, &r1, &g1, &b1); - channels_get_fg_rgb(c2, &r2, &g2, &b2); - channels_get_bg_rgb(c1, &br1, &bg1, &bb1); - channels_get_bg_rgb(c2, &br2, &bg2, &bb2); - int deltr = ((unsigned)r2 - r1) / (len + 1); - int deltg = ((unsigned)g2 - g1) / (len + 1); - int deltb = ((unsigned)b2 - b1) / (len + 1); - int deltbr = ((unsigned)br2 - br1) / (len + 1); - int deltbg = ((unsigned)bg2 - bg1) / (len + 1); - int deltbb = ((unsigned)bb2 - bb1) / (len + 1); + unsigned ur, ug, ub; + int r1, g1, b1, r2, g2, b2; + int br1, bg1, bb1, br2, bg2, bb2; + channels_get_fg_rgb(c1, &ur, &ug, &ub); + r1 = ur; g1 = ug; b1 = ub; + channels_get_fg_rgb(c2, &ur, &ug, &ub); + r2 = ur; g2 = ug; b2 = ub; + channels_get_bg_rgb(c1, &ur, &ug, &ub); + br1 = ur; bg1 = ug; bb1 = ub; + channels_get_bg_rgb(c2, &ur, &ug, &ub); + br2 = ur; bg2 = ug; bb2 = ub; + int deltr = (r2 - r1) / (len + 1); + int deltg = (g2 - g1) / (len + 1); + int deltb = (b2 - b1) / (len + 1); + int deltbr = (br2 - br1) / (len + 1); + int deltbg = (bg2 - bg1) / (len + 1); + int deltbb = (bb2 - bb1) / (len + 1); int ret; cell dupc = CELL_TRIVIAL_INITIALIZER; if(cell_duplicate(n, &dupc, c) < 0){ return -1; } bool fgdef = false, bgdef = false; - if(cell_fg_default_p(c)){ + if(channels_fg_default_p(c1) && channels_fg_default_p(c2)){ fgdef = true; } - if(cell_bg_default_p(c)){ + if(channels_bg_default_p(c1) && channels_bg_default_p(c2)){ bgdef = true; } for(ret = 0 ; ret < len ; ++ret){ @@ -1700,12 +1279,11 @@ int ncplane_hline_interp(ncplane* n, const cell* c, int len, bg1 += deltbg; bb1 += deltbb; if(!fgdef){ - channels_set_fg_rgb(&c1, r1, g1, b1); + cell_set_fg_rgb(&dupc, r1, g1, b1); } if(!bgdef){ - channels_set_bg_rgb(&c1, br1, bg1, bb1); + cell_set_bg_rgb(&dupc, br1, bg1, bb1); } - dupc.channels = c1; if(ncplane_putc(n, &dupc) <= 0){ break; } @@ -1716,18 +1294,23 @@ int ncplane_hline_interp(ncplane* n, const cell* c, int len, int ncplane_vline_interp(ncplane* n, const cell* c, int len, uint64_t c1, uint64_t c2){ - unsigned r1, g1, b1, r2, g2, b2; - unsigned br1, bg1, bb1, br2, bg2, bb2; - channels_get_fg_rgb(c1, &r1, &g1, &b1); - channels_get_fg_rgb(c2, &r2, &g2, &b2); - channels_get_bg_rgb(c1, &br1, &bg1, &bb1); - channels_get_bg_rgb(c2, &br2, &bg2, &bb2); - int deltr = ((unsigned)r2 - r1) / (len + 1); - int deltg = ((unsigned)g2 - g1) / (len + 1); - int deltb = ((unsigned)b2 - b1) / (len + 1); - int deltbr = ((unsigned)br2 - br1) / (len + 1); - int deltbg = ((unsigned)bg2 - bg1) / (len + 1); - int deltbb = ((unsigned)bb2 - bb1) / (len + 1); + unsigned ur, ug, ub; + int r1, g1, b1, r2, g2, b2; + int br1, bg1, bb1, br2, bg2, bb2; + channels_get_fg_rgb(c1, &ur, &ug, &ub); + r1 = ur; g1 = ug; b1 = ub; + channels_get_fg_rgb(c2, &ur, &ug, &ub); + r2 = ur; g2 = ug; b2 = ub; + channels_get_bg_rgb(c1, &ur, &ug, &ub); + br1 = ur; bg1 = ug; bb1 = ub; + channels_get_bg_rgb(c2, &ur, &ug, &ub); + br2 = ur; bg2 = ug; bb2 = ub; + int deltr = (r2 - r1) / (len + 1); + int deltg = (g2 - g1) / (len + 1); + int deltb = (b2 - b1) / (len + 1); + int deltbr = (br2 - br1) / (len + 1); + int deltbg = (bg2 - bg1) / (len + 1); + int deltbb = (bb2 - bb1) / (len + 1); int ret, ypos, xpos; ncplane_cursor_yx(n, &ypos, &xpos); cell dupc = CELL_TRIVIAL_INITIALIZER; @@ -1735,10 +1318,10 @@ int ncplane_vline_interp(ncplane* n, const cell* c, int len, return -1; } bool fgdef = false, bgdef = false; - if(cell_fg_default_p(c)){ + if(channels_fg_default_p(c1) && channels_fg_default_p(c2)){ fgdef = true; } - if(cell_bg_default_p(c)){ + if(channels_bg_default_p(c1) && channels_bg_default_p(c2)){ bgdef = true; } for(ret = 0 ; ret < len ; ++ret){ @@ -1752,12 +1335,11 @@ int ncplane_vline_interp(ncplane* n, const cell* c, int len, bg1 += deltbg; bb1 += deltbb; if(!fgdef){ - channels_set_fg_rgb(&c1, r1, g1, b1); + cell_set_fg_rgb(&dupc, r1, g1, b1); } if(!bgdef){ - channels_set_bg_rgb(&c1, br1, bg1, bb1); + cell_set_bg_rgb(&dupc, br1, bg1, bb1); } - dupc.channels = c1; if(ncplane_putc(n, &dupc) <= 0){ break; } @@ -1772,7 +1354,6 @@ box_corner_needs(unsigned ctlword){ return (ctlword & NCBOXCORNER_MASK) >> NCBOXCORNER_SHIFT; } - int ncplane_box(ncplane* n, const cell* ul, const cell* ur, const cell* ll, const cell* lr, const cell* hl, const cell* vl, int ystop, int xstop, @@ -1803,7 +1384,7 @@ int ncplane_box(ncplane* n, const cell* ul, const cell* ur, if(ncplane_cursor_move_yx(n, yoff, xoff + 1)){ return -1; } - if(!(ctlword & (NCBOXGRAD_TOP << 4u))){ // cell styling, hl + if(!(ctlword & NCBOXGRAD_TOP)){ // cell styling, hl if(ncplane_hline(n, hl, xstop - xoff - 1) < 0){ return -1; } @@ -1830,7 +1411,7 @@ int ncplane_box(ncplane* n, const cell* ul, const cell* ur, if(ncplane_cursor_move_yx(n, yoff, xoff)){ return -1; } - if((ctlword & (NCBOXGRAD_LEFT << 4u))){ // grad styling, ul->ll + if((ctlword & NCBOXGRAD_LEFT)){ // grad styling, ul->ll if(ncplane_vline_interp(n, vl, ystop - yoff, ul->channels, ll->channels) < 0){ return -1; } @@ -1844,7 +1425,7 @@ int ncplane_box(ncplane* n, const cell* ul, const cell* ur, if(ncplane_cursor_move_yx(n, yoff, xstop)){ return -1; } - if((ctlword & (NCBOXGRAD_RIGHT << 4u))){ // grad styling, ur->lr + if((ctlword & NCBOXGRAD_RIGHT)){ // grad styling, ur->lr if(ncplane_vline_interp(n, vl, ystop - yoff, ur->channels, lr->channels) < 0){ return -1; } @@ -1871,7 +1452,7 @@ int ncplane_box(ncplane* n, const cell* ul, const cell* ur, if(ncplane_cursor_move_yx(n, yoff, xoff + 1)){ return -1; } - if(!(ctlword & (NCBOXGRAD_BOTTOM << 4u))){ // cell styling, hl + if(!(ctlword & NCBOXGRAD_BOTTOM)){ // cell styling, hl if(ncplane_hline(n, hl, xstop - xoff - 1) < 0){ return -1; } @@ -1901,8 +1482,11 @@ void ncplane_updamage(ncplane* n){ if(drangehigh > n->nc->stdscr->leny){ drangehigh = n->nc->stdscr->leny; } - if(drangelow < 0){ - drangelow = 0; + if(drangelow < n->nc->stdscr->absy){ + drangelow = n->nc->stdscr->absy; + } + if(drangelow > n->nc->stdscr->absy + n->nc->stdscr->leny - 1){ + drangelow = n->nc->stdscr->absy + n->nc->stdscr->leny - 1; } flash_damage_map(n->nc->damage + drangelow, drangehigh - drangelow, true); } @@ -1911,39 +1495,13 @@ int ncplane_move_yx(ncplane* n, int y, int x){ if(n == n->nc->stdscr){ return -1; } - if(n->absy != y){ - // need to update the damage map of the notcurses object for both our old and - // our new lines - int drange1low = n->absy; - int drange1high = n->absy + n->leny; - if(drange1high > n->nc->stdscr->leny){ - drange1high = n->nc->stdscr->leny; - } - if(drange1low < 0){ - drange1low = 0; - } - int drange2low = y; - int drange2high = y + n->leny; - if(drange2high > n->nc->stdscr->leny){ - drange2high = n->nc->stdscr->leny; - } - if(drange2low < 0){ - drange2low = 0; - } - // must do two distinct flashes in either of these cases, as there's no overlap - if(drange2low > drange1high || drange2high < drange1low){ - flash_damage_map(n->nc->damage + drange1low, drange1high - drange1low, true); - flash_damage_map(n->nc->damage + drange2low, drange2high - drange2low, true); - }else{ - drange1low = drange1low < drange2low ? drange1low : drange2low; - drange1high = drange1high > drange2high ? drange1high : drange2high; - flash_damage_map(n->nc->damage + drange1low, drange1high - drange1low, true); - } - n->absy = y; - }else if(n->absx != x){ + ncplane_updamage(n); // damage any lines we are currently on + bool movedy = n->absy != y; + n->absy = y; + n->absx = x; + if(movedy){ ncplane_updamage(n); } - n->absx = x; return 0; } @@ -1999,6 +1557,9 @@ int ncvisual_render(const ncvisual* ncv){ const int linesize = f->linesize[0]; const unsigned char* data = f->data[0]; for(y = 0 ; y < f->height / 2 && y < dimy ; ++y){ + if(ncplane_cursor_move_yx(ncv->ncp, y, 0)){ + return -1; + } for(x = 0 ; x < f->width && x < dimx ; ++x){ int bpp = av_get_bits_per_pixel(av_pix_fmt_desc_get(f->format)); const unsigned char* rgbbase_up = data + (linesize * (y * 2)) + (x * bpp / CHAR_BIT); @@ -2090,10 +1651,10 @@ void notcurses_cursor_disable(notcurses* nc){ } } -int notcurses_refresh(notcurses* nc){ - if(nc->stats.renders == 0){ - return -1; // haven't rendered yet, and thus don't know what should be there - } - // FIXME - return 0; +ncplane* notcurses_top(notcurses* n){ + return n->top; +} + +ncplane* ncplane_below(ncplane* n){ + return n->z; } diff --git a/src/lib/render.c b/src/lib/render.c new file mode 100644 index 000000000..8c6dd017f --- /dev/null +++ b/src/lib/render.c @@ -0,0 +1,532 @@ +#include +#include +#include +#include "internal.h" + +static inline uint64_t +timespec_to_ns(const struct timespec* t){ + return t->tv_sec * NANOSECS_IN_SEC + t->tv_nsec; +} + +static void +mutex_unlock(void* vlock){ + pthread_mutex_unlock(vlock); +} + +static int +blocking_write(int fd, const char* buf, size_t buflen){ + size_t written = 0; + do{ + ssize_t w = write(fd, buf + written, buflen - written); + if(w < 0){ + if(errno != EAGAIN && errno != EWOULDBLOCK){ + return -1; + } + }else{ + written += w; + } + if(written < buflen){ + struct pollfd pfd = { + .fd = fd, + .events = POLLOUT, + .revents = 0, + }; + poll(&pfd, 1, -1); + } + }while(written < buflen); + return 0; +} + +int notcurses_refresh(notcurses* nc){ + int ret = 0; + pthread_mutex_lock(&nc->lock); + pthread_cleanup_push(mutex_unlock, &nc->lock); + if(nc->mstream == NULL){ + ret = -1; // haven't rendered yet, and thus don't know what should be there + }else if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ + ret = -1; + } + pthread_cleanup_pop(1); + return ret; +} + +static void +update_render_stats(const struct timespec* time1, const struct timespec* time0, + ncstats* stats, int bytes){ + int64_t elapsed = timespec_to_ns(time1) - timespec_to_ns(time0); + //fprintf(stderr, "Rendering took %ld.%03lds\n", elapsed / NANOSECS_IN_SEC, + // (elapsed % NANOSECS_IN_SEC) / 1000000); + if(bytes >= 0){ + stats->render_bytes += bytes; + if(bytes > stats->render_max_bytes){ + stats->render_max_bytes = bytes; + } + if(bytes < stats->render_min_bytes){ + stats->render_min_bytes = bytes; + } + }else{ + ++stats->failed_renders; + } + if(elapsed > 0){ // don't count clearly incorrect information, egads + ++stats->renders; + stats->render_ns += elapsed; + if(elapsed > stats->render_max_ns){ + stats->render_max_ns = elapsed; + } + if(elapsed < stats->render_min_ns){ + stats->render_min_ns = elapsed; + } + } +} + +// determine the best palette for the current frame, and write the necessary +// escape sequences to 'out'. for now, we just assume the ANSI palettes. at +// 256 colors, this is the 16 normal ones, 6x6x6 color cubes, and 32 greys. +// it's probably better to sample the darker regions rather than cover so much +// chroma, but whatever....FIXME +static int +prep_optimized_palette(notcurses* nc, FILE* out __attribute__ ((unused))){ + if(nc->RGBflag){ + return 0; // DirectColor, no need to write palette + } + if(!nc->CCCflag){ + return 0; // can't change palette + } + // FIXME + return 0; +} + +/* +// reshape the shadow framebuffer to match the stdplane's dimensions, throwing +// away the old one. +static int +reshape_shadow_fb(notcurses* nc){ + const size_t size = sizeof(nc->shadowbuf) * nc->stdscr->leny * nc->stdscr->lenx; + cell* fb = malloc(size); + if(fb == NULL){ + return -1; + } + free(nc->shadowbuf); + nc->shadowbuf = fb; + nc->shadowy = nc->stdscr->leny; + nc->shadowx = nc->stdscr->lenx; + memset(fb, 0, size); + return 0; +} +*/ + +// Find the topmost cell for this coordinate by walking down the z-buffer, +// looking for an intersecting ncplane. Once we've found one, check it for +// transparency in either the back- or foreground. If the alpha channel is +// active, keep descending and blending until we hit opacity, or bedrock. We +// recurse to find opacity, and blend the result into what we have. The +// 'findfore' and 'findback' bools control our recursion--there's no point in +// going further down when a color is locked in, so don't (for instance) recurse +// further when we have a transparent foreground and opaque background atop an +// opaque foreground and transparent background. The cell we ultimately return +// (a const ref to 'c') is backed by '*retp' via rawdog copy; the caller must +// not call cell_release() upon it, nor use it beyond the scope of the render. +// +// So, as we go down, we find planes which can have impact on the result. Once +// we've locked the result in (base case), write the deep values we have to 'c'. +// Then, as we come back up, blend them as appropriate. The actual glyph is +// whichever one occurs at the top with a non-transparent α (α < 3). To effect +// tail recursion, though, we instead write first, and then recurse, blending +// as we descend. α <= 0 is opaque. α >= 3 is fully transparent. +static ncplane* +dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, + bool* damage){ + while(p){ + // where in the plane this coordinate would be, based off absy/absx. the + // true origin is 0,0, so abs=2,2 means coordinate 3,3 would be 1,1, while + // abs=-2,-2 would make coordinate 3,3 relative 5, 5. + int poffx, poffy; + poffy = y - p->absy; + poffx = x - p->absx; + if(poffy < p->leny && poffy >= 0){ + if(poffx < p->lenx && poffx >= 0){ // p is valid for this y, x + const cell* vis = &p->fb[fbcellidx(p, poffy, poffx)]; + // if we never loaded any content into the cell (or obliterated it by + // writing in a zero), use the plane's background cell. + if(vis->gcluster == 0){ + vis = &p->defcell; + } + bool lockedglyph = false; + int nalpha; + if(falpha > 0 && (nalpha = cell_get_fg_alpha(vis)) < CELL_ALPHA_TRANS){ + if(c->gcluster == 0){ // never write fully trans glyphs, never replace + if( (c->gcluster = vis->gcluster) ){ // index copy only + lockedglyph = true; // must return this ncplane for this glyph + c->attrword = vis->attrword; + cell_set_fchannel(c, cell_get_fchannel(vis)); // FIXME blend it in + falpha -= (CELL_ALPHA_TRANS - nalpha); // FIXME blend it in + if(p->damage[poffy]){ + *damage = true; + p->damage[poffy] = false; + } + } + } + } + if(balpha > 0 && (nalpha = cell_get_bg_alpha(vis)) < CELL_ALPHA_TRANS){ + cell_set_bchannel(c, cell_get_bchannel(vis)); // FIXME blend it in + balpha -= (CELL_ALPHA_TRANS - nalpha); + if(p->damage[poffy]){ + *damage = true; + p->damage[poffy] = false; + } + } + if((falpha > 0 || balpha > 0) && p->z){ // we must go further! + ncplane* cand = dig_visible_cell(c, y, x, p->z, falpha, balpha, damage); + if(!lockedglyph && cand){ + p = cand; + } + } + return p; + } + } + p = p->z; + } + // should never happen for valid y, x thanks to the stdplane. you fucked up! + return NULL; +} + +static inline ncplane* +visible_cell(cell* c, int y, int x, ncplane* n, bool* damage){ + cell_init(c); + return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANS, CELL_ALPHA_TRANS, damage); +} + +// write the cell's UTF-8 grapheme cluster to the provided FILE*. returns the +// number of columns occupied by this EGC (only an approximation; it's actually +// a property of the font being used). +static int +term_putc(FILE* out, const ncplane* n, const cell* c){ + if(cell_simple_p(c)){ + if(c->gcluster == 0 || iscntrl(c->gcluster)){ +// fprintf(stderr, "[ ]\n"); + if(fputc_unlocked(' ', out) == EOF){ + return -1; + } + }else{ +// fprintf(stderr, "[%c]\n", c->gcluster); + if(fputc_unlocked(c->gcluster, out) == EOF){ + return -1; + } + } + }else{ + const char* ext = extended_gcluster(n, c); +// fprintf(stderr, "[%s]\n", ext); + if(fputs_unlocked(ext, out) < 0){ // FIXME check for short write? + return -1; + } + } + return 0; +} + +// check the current and target style bitmasks against the specified 'stylebit'. +// if they are different, and we have the necessary capability, write the +// applicable terminfo entry to 'out'. returns -1 only on a true error. +static int +term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit, + const char* ton, const char* toff){ + int ret = 0; + unsigned curon = cur & stylebit; + unsigned targon = targ & stylebit; + if(curon != targon){ + if(targon){ + if(ton){ + ret = term_emit("ton", ton, out, false); + } + }else{ + if(toff){ // how did this happen? we can turn it on, but not off? + ret = term_emit("toff", toff, out, false); + } + } + } + if(ret < 0){ + return -1; + } + return 0; +} + +// write any escape sequences necessary to set the desired style +static int +term_setstyles(const notcurses* nc, FILE* out, uint32_t* curattr, const cell* c, + bool* normalized){ + *normalized = false; + uint32_t cellattr = cell_styles(c); + if(cellattr == *curattr){ + return 0; // happy agreement, change nothing + } + int ret = 0; + // if only italics changed, don't emit any sgr escapes. xor of current and + // target ought have all 0s in the lower 8 bits if only italics changed. + if((cellattr ^ *curattr) & 0x00ff0000ul){ + *normalized = true; // FIXME this is pretty conservative + // if everything's 0, emit the shorter sgr0 + if(nc->sgr0 && ((cellattr & CELL_STYLE_MASK) == 0)){ + if(term_emit("sgr0", nc->sgr0, out, false) < 0){ + ret = -1; + } + }else if(term_emit("sgr", tiparm(nc->sgr, cellattr & CELL_STYLE_STANDOUT, + cellattr & CELL_STYLE_UNDERLINE, + cellattr & CELL_STYLE_REVERSE, + cellattr & CELL_STYLE_BLINK, + cellattr & CELL_STYLE_DIM, + cellattr & CELL_STYLE_BOLD, + cellattr & CELL_STYLE_INVIS, + cellattr & CELL_STYLE_PROTECT, 0), + out, false) < 0){ + ret = -1; + } + } + // sgr will blow away italics if they were set beforehand + ret |= term_setstyle(out, *curattr, cellattr, CELL_STYLE_ITALIC, nc->italics, nc->italoff); + *curattr = cellattr; + return ret; +} + +// 3 for foreground, 4 for background, ugh FIXME +static int +term_esc_rgb(notcurses* nc __attribute__ ((unused)), FILE* out, int esc, + unsigned r, unsigned g, unsigned b){ + // The correct way to do this is using tiparm+tputs, but doing so (at least + // as of terminfo 6.1.20191019) both emits ~3% more bytes for a run of 'rgb' + // and gives rise to some corrupted cells (possibly due to special handling of + // values < 256; I'm not at this time sure). So we just cons up our own. + /*if(esc == 4){ + return term_emit("setab", tiparm(nc->setab, (int)((r << 16u) | (g << 8u) | b)), out, false); + }else if(esc == 3){ + return term_emit("setaf", tiparm(nc->setaf, (int)((r << 16u) | (g << 8u) | b)), out, false); + }else{ + return -1; + }*/ + #define RGBESC1 "\x1b" "[" + #define RGBESC2 "8;2;" + // rrr;ggg;bbbm + char rgbesc[] = RGBESC1 " " RGBESC2 " "; + int len = strlen(RGBESC1); + rgbesc[len++] = esc; + len += strlen(RGBESC2); + if(r > 99){ + rgbesc[len++] = r / 100 + '0'; + } + if(r > 9){ + rgbesc[len++] = (r % 100) / 10 + '0'; + } + rgbesc[len++] = (r % 10) + '0'; + rgbesc[len++] = ';'; + if(g > 99){ rgbesc[len++] = g / 100 + '0'; } + if(g > 9){ rgbesc[len++] = (g % 100) / 10 + '0'; } + rgbesc[len++] = g % 10 + '0'; + rgbesc[len++] = ';'; + if(b > 99){ rgbesc[len++] = b / 100 + '0'; } + if(b > 9){ rgbesc[len++] = (b % 100) / 10 + '0'; } + rgbesc[len++] = b % 10 + '0'; + rgbesc[len++] = 'm'; + rgbesc[len] = '\0'; + int w; + if((w = fputs_unlocked(rgbesc, out)) < len){ + return -1; + } + return 0; +} + +static int +term_bg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ + // We typically want to use tputs() and tiperm() to acquire and write the + // escapes, as these take into account terminal-specific delays, padding, + // etc. For the case of DirectColor, there is no suitable terminfo entry, but + // we're also in that case working with hopefully more robust terminals. + // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. + if(nc->RGBflag){ + return term_esc_rgb(nc, out, '4', r, g, b); + }else{ + if(nc->setab == NULL){ + return -1; + } + // For 256-color indexed mode, start constructing a palette based off + // the inputs *if we can change the palette*. If more than 256 are used on + // a single screen, start... combining close ones? For 8-color mode, simple + // interpolation. I have no idea what to do for 88 colors. FIXME + if(nc->colors >= 256){ + term_emit("setab", tiparm(nc->setab, rgb_quantize_256(r, g, b)), out, false); + } + return -1; + } + return 0; +} + +static int +term_fg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ + // We typically want to use tputs() and tiperm() to acquire and write the + // escapes, as these take into account terminal-specific delays, padding, + // etc. For the case of DirectColor, there is no suitable terminfo entry, but + // we're also in that case working with hopefully more robust terminals. + // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. + if(nc->RGBflag){ + return term_esc_rgb(nc, out, '3', r, g, b); + }else{ + if(nc->setaf == NULL){ + return -1; + } + if(nc->colors >= 256){ + term_emit("setaf", tiparm(nc->setaf, rgb_quantize_256(r, g, b)), out, false); + } + // For 256-color indexed mode, start constructing a palette based off + // the inputs *if we can change the palette*. If more than 256 are used on + // a single screen, start... combining close ones? For 8-color mode, simple + // interpolation. I have no idea what to do for 88 colors. FIXME + return -1; + } + return 0; +} + +static inline int +notcurses_render_internal(notcurses* nc){ + int ret = 0; + int y, x; + FILE* out = nc->mstreamfp; + fseeko(out, 0, SEEK_SET); + // don't write a clearscreen. we only update things that have been changed. + // we explicitly move the cursor at the beginning of each output line, so no + // need to home it expliticly. + prep_optimized_palette(nc, out); // FIXME do what on failure? + uint32_t curattr = 0; // current attributes set (does not include colors) + // FIXME as of at least gcc 9.2.1, we get a false -Wmaybe-uninitialized below + // when using these without explicit initializations. for the life of me, i + // can't see any such path, and valgrind is cool with it, so what ya gonna do? + unsigned lastr = 0, lastg = 0, lastb = 0; + unsigned lastbr = 0, lastbg = 0, lastbb = 0; + // we can elide a color escape iff the color has not changed between the two + // cells and the current cell uses no defaults, or if both the current and + // the last used both defaults. + bool fgelidable = false, bgelidable = false, defaultelidable = false; + /*if(nc->stdscr->leny != nc->shadowy || nc->stdscr->lenx != nc->shadowx){ + reshape_shadow_fb(nc); + }*/ + for(y = 0 ; y < nc->stdscr->leny ; ++y){ + bool linedamaged = false; // have we repositioned the cursor to start line? + bool newdamage = nc->damage[y]; +// fprintf(stderr, "nc->damage[%d] (%p) = %u\n", y, nc->damage + y, nc->damage[y]); + if(newdamage){ + nc->damage[y] = 0; + } + // move to the beginning of the line, in case our accounting was befouled + // by wider- (or narrower-) than-reported characters + for(x = 0 ; x < nc->stdscr->lenx ; ++x){ + unsigned r, g, b, br, bg, bb; + ncplane* p; + cell c; // no need to initialize + p = visible_cell(&c, y, x, nc->top, &newdamage); + assert(p); + // don't try to print a wide character on the last column; it'll instead + // be printed on the next line. they probably shouldn't be admitted, but + // we can end up with one due to a resize. + if((x + 1 >= nc->stdscr->lenx && cell_double_wide_p(&c))){ + continue; + } + if(!linedamaged){ + if(newdamage){ + term_emit("cup", tiparm(nc->cup, y, x), out, false); + nc->stats.cellelisions += x; + nc->stats.cellemissions += (nc->stdscr->lenx - x); + linedamaged = true; + }else{ + continue; + } + } + // set the style. this can change the color back to the default; if it + // does, we need update our elision possibilities. + bool normalized; + term_setstyles(nc, out, &curattr, &c, &normalized); + if(normalized){ + defaultelidable = true; + bgelidable = false; + fgelidable = false; + } + // we allow these to be set distinctly, but terminfo only supports using + // them both via the 'op' capability. unless we want to generate the 'op' + // escapes ourselves, if either is set to default, we first send op, and + // then a turnon for whichever aren't default. + + // we can elide the default set iff the previous used both defaults + if(cell_fg_default_p(&c) || cell_bg_default_p(&c)){ + if(!defaultelidable){ + ++nc->stats.defaultemissions; + term_emit("op", nc->op, out, false); + }else{ + ++nc->stats.defaultelisions; + } + // if either is not default, this will get turned off + defaultelidable = true; + fgelidable = false; + bgelidable = false; + } + + // we can elide the foreground set iff the previous used fg and matched + if(!cell_fg_default_p(&c)){ + cell_get_fg_rgb(&c, &r, &g, &b); + if(fgelidable && lastr == r && lastg == g && lastb == b){ + ++nc->stats.fgelisions; + }else{ + term_fg_rgb8(nc, out, r, g, b); + ++nc->stats.fgemissions; + fgelidable = true; + } + lastr = r; lastg = g; lastb = b; + defaultelidable = false; + } + if(!cell_bg_default_p(&c)){ + cell_get_bg_rgb(&c, &br, &bg, &bb); + if(bgelidable && lastbr == br && lastbg == bg && lastbb == bb){ + ++nc->stats.bgelisions; + }else{ + term_bg_rgb8(nc, out, br, bg, bb); + ++nc->stats.bgemissions; + bgelidable = true; + } + lastbr = br; lastbg = bg; lastbb = bb; + defaultelidable = false; + } +// fprintf(stderr, "[%02d/%02d] 0x%02x 0x%02x 0x%02x %p\n", y, x, r, g, b, p); + term_putc(out, p, &c); + if(cell_double_wide_p(&c)){ + ++x; + } + } + if(linedamaged == false){ + nc->stats.cellelisions += x; + } + } + ret |= fflush(out); + fflush(nc->ttyfp); + if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ + ret = -1; + } +/*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions, + fgelisions, fgemissions, bgelisions, bgemissions);*/ + if(nc->renderfp){ + fprintf(nc->renderfp, "%s\n", nc->mstream); + } + return nc->mstrsize; +} + +int notcurses_render(notcurses* nc){ + int ret = 0; + struct timespec start, done; + clock_gettime(CLOCK_MONOTONIC_RAW, &start); + pthread_mutex_lock(&nc->lock); + pthread_cleanup_push(mutex_unlock, &nc->lock); + int bytes = notcurses_render_internal(nc); + int dimy, dimx; + notcurses_resize(nc, &dimy, &dimx); + clock_gettime(CLOCK_MONOTONIC_RAW, &done); + update_render_stats(&done, &start, &nc->stats, bytes); + if(bytes < 0){ + ret = -1; + } + pthread_cleanup_pop(1); + return ret; +} + diff --git a/src/poc/sprite-enhalfen.c b/src/poc/sprite-enhalfen.c new file mode 100644 index 000000000..32ca00e1e --- /dev/null +++ b/src/poc/sprite-enhalfen.c @@ -0,0 +1,135 @@ +#include +#include +#include + +static const char* luigis[] = { +"0000000000000000" +"0000000000000000" +"0000000111110000" +"0000011111120000" +"0000111111220000" +"0000111111111110" +"0000333223222000" +"0003223223322220" +"0003223322222222" +"0033223322232222" +"0033222223333330" +"0003322222333330" +"0000032222222200" +"0000311122200000" +"0003133313000000" +"0003133331300000" +"0033133333112200" +"0031133333332222" +"0031113333332222" +"0001113333333222" +"0001111333333222" +"0001111113331000" +"0001111111111000" +"0001111111113000" +"3333111111131100" +"3333111113311100" +"3333111131111000" +"3333111001111000" +"3333000003333000" +"3300000003333000" +"3000000003333330" +"0000000003333330", + +"0000000000000000" +"0000001111100000" +"0000111111200000" +"0001111112200000" +"0001111111111100" +"0003332232220000" +"0032232233222200" +"0032233222222220" +"0332233222322220" +"0332222233333300" +"0033222223333300" +"0003322222222000" +"0000111122000000" +"0003133113300000" +"0031333311300000" +"0031333311330000" +"0031333311130000" +"0031333332230000" +"0031333322220000" +"0011133322221000" +"0011133322221100" +"0011113322211100" +"0011111133111100" +"0001111133311000" +"0000111333333000" +"0000113333330000" +"0000011333300000" +"0000031113330000" +"0000033330330000" +"0000333330000000" +"0000333333300000" +"0000003333300000", + +"0000001111100000" +"0000111111200000" +"0001111112200000" +"0001111111111100" +"0003332232220000" +"0032232233222200" +"0032233222222220" +"0332233222322220" +"0332222233333300" +"0333222223333300" +"0003322222222000" +"0000033322000000" +"0000111133100020" +"0003333113310222" +"0033333311313222" +"0333333311331222" +"0333333311331323" +"0333333111331330" +"3333331112132300" +"3333111111111000" +"2222211111111000" +"2222211111111003" +"2222111111111033" +"0222111111133333" +"0001311111133333" +"0031131111133333" +"3331113311133333" +"3333111100033333" +"3333310000000000" +"0333000000000000" +"0333000000000000" +"0033300000000000", + +NULL +}; + +static int +enhalfen(const char* s, int width){ + if(strlen(s) % (width * 2)){ + fprintf(stderr, "Bad length: %zu\n", strlen(s)); + return -1; + } + size_t rowoff = 0; + size_t idx; + for(rowoff = 0 ; s[rowoff] ; rowoff += 2 * width){ + for(idx = 0 ; idx < width ; ++idx){ + int c = (s[rowoff + idx] - '0') * 4 + (s[rowoff + width + idx] - '0'); + printf("%x", c); + } + printf("\n"); + } + return 0; +} + +int main(void){ + const char** sprite; + for(sprite = luigis ; *sprite ; ++sprite){ + if(enhalfen(*sprite, 16)){ + return EXIT_FAILURE; + } + printf("\n"); + } + return EXIT_SUCCESS; +} diff --git a/src/poc/unidamage.cpp b/src/poc/unidamage.cpp new file mode 100644 index 000000000..3cf4c9bf5 --- /dev/null +++ b/src/poc/unidamage.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include + +// What happens when we print over half of a wide glyph? + +int main(int argc, char** argv){ + setlocale(LC_ALL, ""); + notcurses_options opts{}; + opts.inhibit_alternate_screen = true; + struct notcurses* nc; + if((nc = notcurses_init(&opts, stdout)) == nullptr){ + return EXIT_FAILURE; + } + struct ncplane* n = notcurses_stdplane(nc); + int dimx, dimy; + ncplane_dim_yx(n, &dimy, &dimx); + cell c = CELL_TRIVIAL_INITIALIZER; + cell_set_bg_rgb(&c, 0, 0x80, 0); + //ncplane_set_default(n, &c); + if(cell_load(n, &c, "🐳") < 0){ + goto err; + } + if(dimy > 5){ + dimy = 5; + } + for(int i = 0 ; i < dimy ; ++i){ + for(int j = 8 ; j < dimx / 2 ; ++j){ // leave some empty spaces + if(ncplane_putc_yx(n, i, j * 2, &c) < 0){ + goto err; + } + } + } + ncplane_putc_yx(n, dimy, dimx - 3, &c); + ncplane_putc_yx(n, dimy, dimx - 1, &c); + ncplane_putc_yx(n, dimy + 1, dimx - 2, &c); + ncplane_putc_yx(n, dimy + 1, dimx - 4, &c); + cell_release(n, &c); + // put these on the right side of the wide glyphs + for(int i = 0 ; i < dimy / 2 ; ++i){ + for(int j = 5 ; j < dimx / 2 ; j += 2){ + if(ncplane_putsimple_yx(n, i, j, (j % 10) + '0') < 0){ + goto err; + } + } + } + // put these on the left side of the wide glyphs + for(int i = dimy / 2 ; i < dimy ; ++i){ + for(int j = 4 ; j < dimx / 2 ; j += 2){ + if(ncplane_putsimple_yx(n, i, j, (j % 10) + '0') < 0){ + goto err; + } + } + } + if(notcurses_render(nc)){ + goto err; + } + printf("\n"); + return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS; + +err: + notcurses_stop(nc); + return EXIT_FAILURE; +} diff --git a/tests/bob.mkv b/tests/bob.mkv deleted file mode 100644 index 1f4fded25..000000000 Binary files a/tests/bob.mkv and /dev/null differ diff --git a/tests/fm6.mkv b/tests/fm6.mkv new file mode 100644 index 000000000..2403369db Binary files /dev/null and b/tests/fm6.mkv differ diff --git a/tests/internal.cpp b/tests/internal.cpp index d47122d00..3840b2f53 100644 --- a/tests/internal.cpp +++ b/tests/internal.cpp @@ -8,17 +8,74 @@ class InternalsTest : public :: testing::Test { void SetUp() override { setlocale(LC_ALL, ""); } - }; TEST_F(InternalsTest, RGBtoANSIWhite) { unsigned r, g, b; - r = g = b = 0xff; - EXPECT_EQ(255, rgb_to_ansi256(r, g, b)); + for(r = 250 ; r < 256 ; ++r){ + g = b = r; + EXPECT_EQ(15, rgb_quantize_256(r, g, b)); + } } TEST_F(InternalsTest, RGBtoANSIBlack) { unsigned r, g, b; - r = g = b = 0x0; - EXPECT_EQ(232, rgb_to_ansi256(r, g, b)); + for(r = 0 ; r < 10 ; ++r){ + g = b = r; + EXPECT_EQ(0, rgb_quantize_256(r, g, b)); + } +} + +TEST_F(InternalsTest, RGBtoANSIGrey) { + unsigned r, g, b; + for(r = 10 ; r < 244 ; ++r){ + g = b = r; + EXPECT_EQ(231 + (r * 5) / 49, rgb_quantize_256(r, g, b)); + } +} + +// Pure reds are either 0 (black), or 16 plus 36 * [0..5]. +TEST_F(InternalsTest, RGBtoANSIRed) { + unsigned r, g, b; + g = b = 0x0; + for(r = 0 ; r < 256 ; ++r){ + int c256 = rgb_quantize_256(r, g, b); + if(r < 8){ + EXPECT_EQ(0, c256); + }else{ + EXPECT_LT(15, c256); + EXPECT_EQ(16, c256 % 36); + } + } +} + +// Pure greens are either 0 (black), or 16 plus 6 * [0..5]. +TEST_F(InternalsTest, RGBtoANSIGreen) { + unsigned r, g, b; + r = b = 0x0; + for(g = 0 ; g < 256 ; ++g){ + int c256 = rgb_quantize_256(r, g, b); + EXPECT_GT(48, c256); + if(g < 8){ + EXPECT_EQ(0, c256); + }else{ + EXPECT_LT(15, c256); + EXPECT_EQ(4, c256 % 6); + } + } +} + +// Pure blues are either 0 (black), or one of the first 6 colors [16..22]. +TEST_F(InternalsTest, RGBtoANSIBlue) { + unsigned r, g, b; + r = g = 0x0; + for(b = 0 ; b < 256 ; ++b){ + int c256 = rgb_quantize_256(r, g, b); + EXPECT_GT(22, c256); + if(b < 8){ + EXPECT_EQ(0, c256); + }else{ + EXPECT_LT(15, c256); + } + } } diff --git a/tests/libav.cpp b/tests/libav.cpp index 9908162d7..74e9d8ec0 100644 --- a/tests/libav.cpp +++ b/tests/libav.cpp @@ -57,7 +57,7 @@ TEST_F(LibavTest, LoadVideo) { int averr; int dimy, dimx; ncplane_dim_yx(ncp_, &dimy, &dimx); - auto ncv = ncplane_visual_open(ncp_, "../tests/bob.mkv", &averr); + auto ncv = ncplane_visual_open(ncp_, "../tests/fm6.mkv", &averr); ASSERT_NE(nullptr, ncv); EXPECT_EQ(0, averr); auto frame = ncvisual_decode(ncv, &averr); @@ -74,7 +74,7 @@ TEST_F(LibavTest, LoadVideoCreatePlane) { int averr; int dimy, dimx; ncplane_dim_yx(ncp_, &dimy, &dimx); - auto ncv = ncvisual_open_plane(nc_, "../tests/bob.mkv", &averr, 0, 0, false); + auto ncv = ncvisual_open_plane(nc_, "../tests/fm6.mkv", &averr, 0, 0, false); ASSERT_NE(nullptr, ncv); EXPECT_EQ(0, averr); auto frame = ncvisual_decode(ncv, &averr); diff --git a/tests/ncplane.cpp b/tests/ncplane.cpp index 886f0972e..ee954b73b 100644 --- a/tests/ncplane.cpp +++ b/tests/ncplane.cpp @@ -762,3 +762,116 @@ TEST_F(NcplaneTest, MoveToLowerRight) { EXPECT_EQ(0, ncplane_destroy(ncp)); // FIXME verify with ncplane_at_cursor() } + +// Placing a wide char to the immediate left of any other char ought obliterate +// that cell. +TEST_F(NcplaneTest, WideCharAnnihilatesRight) { + const wchar_t* w = L"🐸"; + const wchar_t* wbashed = L"🦂"; + const char bashed = 'X'; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, wbashed, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putsimple_yx(n_, 1, 1, bashed)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(1, y); + EXPECT_EQ(2, x); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, w, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 1, 0, w, 0, 0, &sbytes)); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 1, 0, &c); + wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog + ncplane_at_yx(n_, 1, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); //should be wide + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(0, c.gcluster); + EXPECT_EQ(0, notcurses_render(nc_)); // should be nothing +} + +// Placing a wide char on the right half of a wide char ought obliterate the +// original wide char. +TEST_F(NcplaneTest, WideCharAnnihilatesWideLeft) { + const wchar_t* w = L"🐍"; + const wchar_t* wbashed = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashed, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, w, 0, 0, &sbytes)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 0, 1, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide + EXPECT_EQ(0, notcurses_render(nc_)); +} + +// Placing a normal char on either half of a wide char ought obliterate +// the original wide char. +TEST_F(NcplaneTest, WideCharsAnnihilated) { + const char cc = 'X'; + const wchar_t* wbashedl = L"🐍"; + const wchar_t* wbashedr = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashedl, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 2, wbashedr, 0, 0, &sbytes)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 1, cc)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X' + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X" + ncplane_at_yx(n_, 0, 3, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + EXPECT_EQ(0, notcurses_render(nc_)); +} + +// But placing something to the immediate right of any glyph, that is not a +// problem. Ensure it is so. +TEST_F(NcplaneTest, AdjacentCharsSafe) { + const char cc = 'X'; + const wchar_t* wsafel = L"🐍"; + const wchar_t* wsafer = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wsafel, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 3, wsafer, 0, 0, &sbytes)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be snake + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X' + ncplane_at_yx(n_, 0, 3, &c); + wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🦂")); // should be scorpion + ncplane_at_yx(n_, 0, 4, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be scorpion + EXPECT_EQ(0, notcurses_render(nc_)); +} diff --git a/tests/zaxis.cpp b/tests/zaxis.cpp new file mode 100644 index 000000000..1e3bd6eb6 --- /dev/null +++ b/tests/zaxis.cpp @@ -0,0 +1,136 @@ +#include "main.h" +#include +#include + +class ZAxisTest : public :: testing::Test { + protected: + void SetUp() override { + setlocale(LC_ALL, ""); + if(getenv("TERM") == nullptr){ + GTEST_SKIP(); + } + notcurses_options nopts{}; + nopts.inhibit_alternate_screen = true; + outfp_ = fopen("/dev/tty", "wb"); + ASSERT_NE(nullptr, outfp_); + nc_ = notcurses_init(&nopts, outfp_); + ASSERT_NE(nullptr, nc_); + n_ = notcurses_stdplane(nc_); + ASSERT_NE(nullptr, n_); + } + + void TearDown() override { + if(nc_){ + EXPECT_EQ(0, notcurses_stop(nc_)); + } + if(outfp_){ + fclose(outfp_); + } + } + + struct notcurses* nc_{}; + struct ncplane* n_{}; + FILE* outfp_{}; +}; + +TEST_F(ZAxisTest, StdPlaneOnly) { + struct ncplane* top = notcurses_top(nc_); + EXPECT_EQ(n_, top); + EXPECT_EQ(nullptr, ncplane_below(top)); +} + +// if you want to move the plane which is already top+bottom to either, go ahead +TEST_F(ZAxisTest, StdPlaneOnanism) { + EXPECT_EQ(0, ncplane_move_top(n_)); + struct ncplane* top = notcurses_top(nc_); + EXPECT_EQ(n_, top); + EXPECT_EQ(nullptr, ncplane_below(top)); + EXPECT_EQ(0, ncplane_move_bottom(n_)); + EXPECT_EQ(nullptr, ncplane_below(n_)); +} + +// you can't place a plane above or below itself, stdplane or otherwise +TEST_F(ZAxisTest, NoMoveSelf) { + struct ncplane* np = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_NE(nullptr, np); + EXPECT_NE(0, ncplane_move_below(n_, n_)); + EXPECT_NE(0, ncplane_move_above(n_, n_)); + EXPECT_NE(0, ncplane_move_below(np, np)); + EXPECT_NE(0, ncplane_move_above(np, np)); +} + +// new planes ought be on the top +TEST_F(ZAxisTest, NewPlaneOnTop) { + struct ncplane* np = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_NE(nullptr, np); + struct ncplane* top = notcurses_top(nc_); + EXPECT_EQ(np, top); + EXPECT_EQ(n_, ncplane_below(top)); + EXPECT_EQ(nullptr, ncplane_below(n_)); +} + +// "move" top plane to top. everything ought remain the same. +TEST_F(ZAxisTest, TopToTop) { + struct ncplane* np = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_NE(nullptr, np); + struct ncplane* top = notcurses_top(nc_); + EXPECT_EQ(np, top); + EXPECT_EQ(n_, ncplane_below(top)); + EXPECT_EQ(nullptr, ncplane_below(n_)); + EXPECT_EQ(0, ncplane_move_top(np)); + // verify it + top = notcurses_top(nc_); + EXPECT_EQ(np, top); + EXPECT_EQ(n_, ncplane_below(top)); + EXPECT_EQ(nullptr, ncplane_below(n_)); +} + +// move top plane to bottom, and verify enumeration +TEST_F(ZAxisTest, TopToBottom) { + struct ncplane* np = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_NE(nullptr, np); + struct ncplane* top = notcurses_top(nc_); + EXPECT_EQ(np, top); + EXPECT_EQ(n_, ncplane_below(top)); + EXPECT_EQ(nullptr, ncplane_below(n_)); + EXPECT_EQ(0, ncplane_move_bottom(np)); + top = notcurses_top(nc_); + EXPECT_EQ(n_, top); + EXPECT_EQ(np, ncplane_below(top)); + EXPECT_EQ(nullptr, ncplane_below(np)); +} + +// verify that moving one above another, with no other changes, is reflected at +// render time (requires explicit damage maintenance from move functionality). +TEST_F(ZAxisTest, ZAxisDamage) { + cell cat = CELL_TRIVIAL_INITIALIZER; + cell c = CELL_SIMPLE_INITIALIZER('x'); + ASSERT_EQ(0, cell_set_fg_rgb(&c, 0xff, 0, 0)); + ASSERT_EQ(1, ncplane_putc(n_, &c)); + EXPECT_EQ(0, notcurses_render(nc_)); + ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0)); + ASSERT_EQ(1, ncplane_at_cursor(n_, &cat)); + ASSERT_TRUE(cell_simple_p(&cat)); + ASSERT_EQ('x', cat.gcluster); + struct ncplane* n2 = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_EQ(1, cell_load(n2, &c, "y")); + ASSERT_EQ(0, cell_set_fg_rgb(&c, 0, 0xff, 0)); + ASSERT_EQ(1, ncplane_putc(n2, &c)); + EXPECT_EQ(0, notcurses_render(nc_)); + ASSERT_EQ(0, ncplane_cursor_move_yx(n2, 0, 0)); + ASSERT_EQ(1, ncplane_at_cursor(n2, &cat)); + ASSERT_EQ('y', cat.gcluster); + struct ncplane* n3 = notcurses_newplane(nc_, 2, 2, 0, 0, nullptr); + ASSERT_EQ(1, cell_load(n3, &c, "z")); + ASSERT_EQ(0, cell_set_fg_rgb(&c, 0, 0, 0xff)); + ASSERT_EQ(1, ncplane_putc(n3, &c)); + EXPECT_EQ(0, notcurses_render(nc_)); + ASSERT_EQ(0, ncplane_cursor_move_yx(n3, 0, 0)); + ASSERT_EQ(1, ncplane_at_cursor(n3, &cat)); + ASSERT_EQ('z', cat.gcluster); + // FIXME testing damage requires notcurses keeping a copy of the screen.... + // FIXME move y atop z + // FIXME inspect + // FIXME move z atop y + // FIXME inspect +}