Wide character rigor (#117) (#157)

* unidamage PoC
* add cell_load_simple()
* clear CELL_WIDEASIAN_MASK in cell_load()
* split out render code
* add CELL_SIMPLE_INITIALIZER
* widecolor: fix message plane
* widecolor: simplify color increments
* document wide character handling
* unit tests for wide obliteration #117
* widechar obliteration hardening #117
* widecolor -> widechomper, update man page
This commit is contained in:
Nick Black 2019-12-18 04:38:43 -05:00 committed by GitHub
parent 040607c6f9
commit 957549105b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 865 additions and 611 deletions

View File

@ -14,8 +14,8 @@ by [nick black](https://nick-black.com/dankwiki/index.php/Hack_on) (<nickblack@l
* [Requirements](#requirements)
* [Use](#use)
* [Input](#input)
* [Planes](#planes)
* [Cells](#cells)
* [Planes](#planes) ([Plane Channels API](#plane-channels-api), [Wide chars](#wide-chars))
* [Cells](#cells) ([Cell Channels API](#cell-channels-api))
* [Multimedia](#multimedia)
* [Panelreels](#panelreels)
* [Channels](#channels)
@ -467,6 +467,10 @@ not necessarily reflect anything on the actual screen).
// it in 'c'. This copy is safe to use until the ncplane is destroyed/erased.
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.
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.
@ -868,6 +872,32 @@ void ncplane_set_fg_default(struct ncplane* n);
void ncplane_set_bg_default(struct ncplane* n);
```
#### Wide chars
Notcurses assumes that all glyphs occupy widths which are an integral multiple
of the smallest possible glyph's cell width (aka a "fixed-width font"). Unicode
introduces characters which generally occupy two such cells, known as wide
characters (though in the end, width of a glyph is a property of the font). It
is not possible to print half of such a glyph, nor is it generally possible to
print a wide glyph on the last column of a terminal.
Notcurses does not consider it an error to place a wide character on the last
column of a line. It will obliterate any content which was in that cell, but
will not itself be rendered. The default content will not be reproduced in such
a cell, either. When any character is placed atop a wide character's left or
right half, the wide character is obliterated in its entirety. When a wide
character is placed, any character under its left or right side is annihilated,
including wide characters. It is thus possible for two wide characters to sit
at columns 0 and 2, and for both to be obliterated by a single wide character
placed at column 1.
Likewise, when rendering, a plane which would partially obstruct a wide glyph
prevents it from being rendered entirely. A pathological case would be that of
a terminal _n_ columns in width, containing _n-1_ planes, each 2 columns wide.
The planes are placed at offsets [0..n - 2]. Each plane is above the plane to
its left, and each plane contains a single wide character. Were this to be
rendered, only the rightmost plane (and its single glyph) would be rendered!
### Cells
Unlike the `notcurses` or `ncplane` objects, the definition of `cell` is
@ -932,12 +962,14 @@ typedef struct cell {
#define CELL_ALPHA_OPAQUE 0
```
`cell`s must be initialized with `CELL_TRIVIAL_INITIALIZER` or `cell_init()`
before any other use (both merely zero out the `cell`).
`cell`s must be initialized with an initialization macro or `cell_init()`
before any other use. `cell_init()` and `CELL_TRIVIAL_INITIALIZER` both
simply zero out the `cell`.
```c
#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){
@ -1036,6 +1068,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

View File

@ -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

View File

@ -379,6 +379,10 @@ API struct ncplane* ncplane_below(struct ncplane* n);
// 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.
@ -1101,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){
@ -1201,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

View File

@ -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);
}
@ -156,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);

View File

@ -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);

View File

@ -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);

View File

@ -7,6 +7,9 @@
#include <pthread.h>
#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);
}

View File

@ -197,6 +197,31 @@ rgb_quantize_256(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

View File

@ -2,7 +2,6 @@
#include <time.h>
#include <term.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
@ -157,40 +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, 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;
}
}
}
static const char NOTCURSES_VERSION[] =
notcurses_VERSION_MAJOR "."
notcurses_VERSION_MINOR "."
@ -238,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;
@ -295,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.
@ -931,139 +888,10 @@ 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(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 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_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;
}
static void
advance_cursor(ncplane* n, int cols){
if(cursor_invalid_p(n)){
@ -1071,156 +899,10 @@ 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_unsafe(ncplane* restrict n, ncplane* restrict above){
ncplane** an = find_above_ncplane(n);
@ -1279,232 +961,6 @@ int ncplane_move_bottom(ncplane* 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;
}
/*
// 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;
}
*/
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;
}
static void
mutex_unlock(void* vlock){
pthread_mutex_unlock(vlock);
}
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;
}
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;
}
int ncplane_cursor_move_yx(ncplane* n, int y, int x){
if(x >= n->lenx || x < 0){
return -1;
@ -1549,18 +1005,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);
@ -1568,11 +1060,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;
}
@ -1622,6 +1110,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;
}
@ -2053,6 +1542,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);

532
src/lib/render.c Normal file
View File

@ -0,0 +1,532 @@
#include <ctype.h>
#include <unistd.h>
#include <sys/poll.h>
#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;
}

View File

@ -18,19 +18,38 @@ int main(int argc, char** argv){
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(ncplane_set_default(n, &c) < 0){
goto err;
}
cell_release(n, &c);
if(cell_load(n, &c, "x") < 0){
goto err;
if(dimy > 5){
dimy = 5;
}
for(int i = 0 ; i < dimy ; ++i){
for(int j = 0 ; j < dimx / 2 ; j += 2){
if(ncplane_putc_yx(n, i, j, &c) < 0){
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;
}
}
@ -38,6 +57,7 @@ int main(int argc, char** argv){
if(notcurses_render(nc)){
goto err;
}
printf("\n");
return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS;
err:

View File

@ -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_));
}