You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/internal.h

1897 lines
69 KiB
C

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#ifndef NOTCURSES_INTERNAL
#define NOTCURSES_INTERNAL
#ifdef __cplusplus
extern "C" {
#endif
#include "version.h"
#include "builddef.h"
#include "compat/compat.h"
#include "notcurses/ncport.h"
#include "notcurses/notcurses.h"
#include "notcurses/direct.h"
#ifndef __MINGW32__
#define API __attribute__((visibility("default")))
#else
#define API __declspec(dllexport)
#endif
#define ALLOC __attribute__((malloc)) __attribute__((warn_unused_result))
// KEY_EVENT is defined by both ncurses.h (prior to 6.3) and wincon.h. since we
// don't use either definition, kill it before inclusion of ncurses.h.
#undef KEY_EVENT
#include <term.h>
#include <time.h>
#include <term.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <wctype.h>
#include <pthread.h>
#include <stdbool.h>
#include <inttypes.h>
#include <unictype.h>
#ifndef __MINGW32__
#include <langinfo.h>
#endif
#include "lib/termdesc.h"
#include "lib/egcpool.h"
#include "lib/sprite.h"
#include "lib/fbuf.h"
#include "lib/gpm.h"
struct sixelmap;
struct ncvisual_details;
// Was this glyph drawn as part of an ncvisual? If so, we need to honor
// blitter stacking rather than the standard trichannel solver.
#define NC_BLITTERSTACK_MASK NC_NOBACKGROUND_MASK
// we can't define multipart ncvisual here, because OIIO requires C++ syntax,
// and we can't go throwing C++ syntax into this header. so it goes.
// A plane is memory for some rectilinear virtual window, plus current cursor
// state for that window, and part of a pile. Each pile has a total order along
// its z-axis. Functions update these virtual planes over a series of API
// calls. Eventually, notcurses_render() is called. We then do a depth buffer
// blit of updated cells. A cell is updated if the topmost plane including that
// cell updates it, not simply if any plane updates it.
//
// A plane may be partially or wholly offscreen--this might occur if the
// screen is resized, for example. Offscreen portions will not be rendered.
// Accesses beyond the borders of a panel, however, are errors.
//
// The framebuffer 'fb' is a set of rows. For scrolling, we interpret it as a
// circular buffer of rows. 'logrow' is the index of the row at the logical top
// of the plane. It only changes from 0 if the plane is scrollable.
typedef struct ncplane {
nccell* fb; // "framebuffer" of character cells
int logrow; // logical top row, starts at 0, add one for each scroll
unsigned x, y; // current cursor location within this plane
// ncplane_yx() etc. use coordinates relative to the plane to which this
// plane is bound, but absx/absy are always relative to the terminal origin.
// they must thus be translated by any function which moves a parent plane.
int absx, absy; // origin of the plane relative to the pile's origin
// also used as left and top margin on resize by
// ncplane_resize_marginalized()
unsigned lenx, leny; // size of the plane, [0..len{x,y}) is addressable
egcpool pool; // attached storage pool for UTF-8 EGCs
uint64_t channels; // works the same way as cells
// a notcurses context is made up of piles, each rooted by one or more root
// planes. each pile has its own (totally ordered) z-axis.
struct ncpile* pile; // pile of which we are a part
struct ncplane* above; // plane above us, NULL if we're on top
struct ncplane* below; // plane below us, NULL if we're on bottom
// every plane is bound to some other plane, unless it is a root plane of a
// pile. a pile has a set of one or more root planes, all of them siblings.
// root planes are bound to themselves. the standard plane is always a root
// plane (since it cannot be reparented). a path exists to a root plane.
struct ncplane* bnext; // next in the blist
struct ncplane** bprev; // blist link back to us
struct ncplane* blist; // head of list of bound planes
struct ncplane* boundto;// plane to which we are bound (ourself for roots)
sprixel* sprite; // pointer into the sprixel cache
tament* tam; // transparency-annihilation sprite matrix
void* userptr; // slot for the user to stick some opaque pointer
int (*resizecb)(struct ncplane*); // callback after parent is resized
nccell basecell; // cell written anywhere that fb[i].gcluster == 0
char* name; // used only for debugging
ncalign_e halign; // relative to parent plane, for automatic realignment
ncalign_e valign; // relative to parent plane, for automatic realignment
uint16_t stylemask; // same deal as in a cell
int margin_b, margin_r;// bottom and right margins, stored for resize
bool scrolling; // is scrolling enabled? always disabled by default
bool fixedbound; // are we fixed relative to the parent's scrolling?
bool autogrow; // do we grow to accommodate output?
// we need to track any widget to which we are bound, so that (1) we don't
// end up bound to two widgets and (2) we can clean them up on shutdown
// (assuming they weren't explicitly cleaned up by the client).
void* widget; // widget to which we are bound, can be NULL
void(*wdestruct)(void*); // widget destructor, NULL iff widget is NULL
} ncplane;
// current presentation state of the terminal. it is carried across render
// instances. initialize everything to 0 on a terminal reset / startup.
typedef struct rasterstate {
// we assemble the encoded (rasterized) output in an fbuf (a portable POSIX
// memstream, basically), and keep it around between uses. this could be a
// problem if it ever tremendously spiked, but that seems unlikely?
fbuf f; // buffer for preparing raster glyph/escape stream
// the current cursor position. this is independent of whether the cursor is
// visible. it is the cell at which the next write will take place. this is
// modified by: output, cursor moves, clearing the screen (during refresh).
int y, x;
const ncplane* lastsrcp; // last source plane (we emit hpa on plane changes)
unsigned lastr; // foreground rgb, overloaded for palindexed fg
unsigned lastg;
unsigned lastb;
unsigned lastbr; // background rgb, overloaded for palindexed bg
unsigned lastbg;
unsigned lastbb;
// used in CLI mode, these track the end of logical output, to place the
// cursor following each rasterization. they are tracked thusly:
// * initialized to the initial physical cursor position
// * when we write to a physical row greater than logendy, update both
// * when we scroll, subtract one from logendy
// * if logendy reaches -1, reset both to 0
int logendy, logendx;
uint16_t curattr; // current attributes set (does not include colors)
// we elide a color escape iff the color has not changed between two cells
bool fgelidable;
bool bgelidable;
bool fgpalelidable;
bool bgpalelidable;
bool fgdefelidable;
bool bgdefelidable;
} rasterstate;
// Tablets are the toplevel entitites within an ncreel. Each corresponds to
// a single, distinct ncplane.
typedef struct nctablet {
ncplane* p; // border plane, NULL when offscreen
ncplane* cbp; // data plane, NULL when offscreen
struct nctablet* next;
struct nctablet* prev;
tabletcb cbfxn; // application callback to draw cbp
void* curry; // application data provided to cbfxn
} nctablet;
typedef struct ncreel {
ncplane* p; // ncplane this ncreel occupies, under tablets
// doubly-linked list, a circular one when infinity scrolling is in effect.
// points at the focused tablet (when at least one tablet exists, one must be
// focused). it will be visibly focused following the next redraw.
nctablet* tablets;
nctablet* vft; // the visibly-focused tablet
enum {
LASTDIRECTION_UP,
LASTDIRECTION_DOWN,
} direction; // last direction of travel
int tabletcount; // could be derived, but we keep it o(1)
ncreel_options ropts; // copied in ncreel_create()
} ncreel;
typedef struct ncfdplane {
ncfdplane_callback cb; // invoked with fresh hot data
ncfdplane_done_cb donecb; // invoked on EOF (if !follow) or error
void* curry; // passed to the callbacks
int fd; // we take ownership of the fd, and close it
bool follow; // keep trying to read past the end (event-based)
ncplane* ncp; // bound ncplane
pthread_t tid; // thread servicing this i/o
bool destroyed; // set in ncfdplane_destroy() in our own context
} ncfdplane;
typedef struct ncsubproc {
ncfdplane* nfp;
pid_t pid; // subprocess
int pidfd; // for signalling/watching the subprocess
pthread_t waittid; // wait()ing thread if pidfd is not available
pthread_mutex_t lock; // guards waited
bool waited; // we've wait()ed on it, don't kill/wait further
} ncsubproc;
typedef struct ncreader {
ncplane* ncp; // always owned by ncreader
uint64_t tchannels; // channels for input text
uint32_t tattrs; // attributes for input text
ncplane* textarea; // grows as needed iff scrolling is enabled
int xproject; // virtual x location of ncp origin on textarea
bool horscroll; // is there horizontal panning?
bool no_cmd_keys; // are shortcuts disabled?
bool manage_cursor; // enable and place a virtual cursor
} ncreader;
typedef struct ncprogbar {
ncplane* ncp;
double progress; // on the range [0, 1]
uint32_t ulchannel, urchannel, blchannel, brchannel;
bool retrograde;
} ncprogbar;
typedef struct nctab {
struct nctabbed* nt; // The nctabbed this belongs to
tabcb cb; // tab callback
char* name; // tab name
int namecols; // tab name width in columns
void* curry; // user pointer
struct nctab* prev;
struct nctab* next;
} nctab;
// various moving parts within a notcurses context (and the user) might need to
// access the stats object, so throw a lock on it. we don't want the lock in
// the actual structure since (a) it's usually unnecessary and (b) it breaks
// memset() and memcpy().
typedef struct ncsharedstats {
pthread_mutex_t lock;
ncstats s;
} ncsharedstats;
typedef struct ncdirect {
ncpalette palette; // 256-indexed palette can be used instead of/with RGB
FILE* ttyfp; // FILE* for output tty
tinfo tcache; // terminfo cache
uint64_t channels; // current channels
uint16_t stylemask; // current styles
uint64_t flags; // copied in ncdirect_init() from param
ncsharedstats stats; // stats! not as broadly used as in notcurses
} ncdirect;
// Extracellular state for a cell during the render process. There is one
// crender per rendered cell, and they are initialized to all zeroes.
struct crender {
const ncplane *p; // source of glyph for this cell
nccell c;
uint32_t hcfg; // fg channel prior to HIGHCONTRAST (need full channel)
sprixel* sprixel; // bitmap encountered during traversal
struct {
// If the glyph we render is from an ncvisual, and has a transparent or
// blended background, blitter stacking is in effect. This is a complicated
// issue, but essentially, imagine a bottom block is rendered with a green
// bottom and transparent top. on a lower plane, a top block is rendered
// with a red foreground and blue background. Normally, this would result
// in a blue top and green bottom, but that's not what we ever wanted --
// what makes sense is a red top and green bottom. So ncvisual rendering
// sets bits from CELL_BLITTERSTACK_MASK when rendering a cell with a
// transparent background. When paint() selects a glyph, it checks for these
// bits. If they are set, any lower planes with CELL_BLITTERSTACK_MASK set
// take this into account when solving the background color.
unsigned blittedquads: 4;
unsigned damaged: 1; // only used in rasterization
// if NCALPHA_HIGHCONTRAST is in play, we apply the HSV flip once the
// background is locked in. set highcontrast to indicate this.
unsigned highcontrast: 1;
unsigned fgblends: 8;
unsigned bgblends: 8;
// we'll need recalculate the foreground relative to the solved background,
// and then reapply any foreground shading from above the highcontrast
// declaration. save the foreground state when we go highcontrast.
unsigned hcfgblends: 8; // number of foreground blends prior to HIGHCONTRAST
unsigned sprixeled: 1; // have we passed through a sprixel?
unsigned p_beats_sprixel: 1; // did we solve for our glyph above the bitmap?
} s;
};
// a pile is a collection of planes which will be rendered together. piles are
// completely distinct with regards to thread-safety; one can always operate
// concurrently on distinct piles (save rasterizing, of course). material from
// other piles will be blown away whenever a pile is rasterized. no ordering
// is meaningful between planes. when a new one is added, or one is destroyed,
// the pilelock (in struct notcurses) is taken. a pile is destroyed whenever
// its last plane is destroyed or reparented away. a pile contains a totally-
// ordered list of piles (ordered on the z axis) and a directed acyclic forest
// of bound planes, with each root plane rooting a DAG.
//
// the geometries are updated each time the pile is rendered. until a pile is
// rendered, its geometries might be out-of-date with regards to the terminal
// description in tcache. when it is rendered again, the geometries will be
// updated, sprixels will have their TAMs rebuilt (if necessary), and each
// root plane will have its resize callback invoked (possibly invoking its
// bound planes' resize callbacks in turn).
//
// at context start, there is one pile (the standard pile), containing one
// plane (the standard plane). each ncplane holds a pointer to its pile.
typedef struct ncpile {
ncplane* top; // topmost plane, never NULL
ncplane* bottom; // bottommost plane, never NULL
ncplane* roots; // head of root plane list
struct crender* crender; // array (rows * cols crender objects)
struct notcurses* nc; // notcurses context
struct ncpile *prev, *next; // circular list pointers
size_t crenderlen; // size of crender vector
unsigned dimy, dimx; // rows and cols at last render/creation
unsigned cellpxx, cellpxy; // cell-pixel geometry at last render/creation
int scrolls; // how many real lines need be scrolled at raster
sprixel* sprixelcache; // sorted list of sprixels, assembled during paint
} ncpile;
// the standard pile can be reached through ->stdplane.
typedef struct notcurses {
ncplane* stdplane; // standard plane, covers screen
// the style state of the terminal is carried across render runs
rasterstate rstate;
// we keep a copy of the last rendered frame. this facilitates O(1)
// notcurses_at_yx() and O(1) damage detection (at the cost of some memory).
// FIXME why isn't this just an ncplane rather than ~10 different members?
nccell* lastframe;// last rasterized framebuffer, NULL until first raster
// the last pile we rasterized. NULL until we've rasterized once. might
// be invalid due to the pile being destroyed; you are only allowed to
// evaluate it for equality to the pile being currently rasterized. when
// we switch piles, we need to clear all displayed sprixels, and
// invalidate the new pile's, pursuant to their display.
ncpile* last_pile;
egcpool pool; // egcpool for lastframe
unsigned lfdimx; // dimensions of lastframe, unchanged by screen resize
unsigned lfdimy; // lfdimx/lfdimy are 0 until first rasterization
int cursory; // desired cursor placement according to user.
int cursorx; // -1 is don't-care, otherwise moved here after each render.
ncsharedstats stats; // some statistics across the lifetime of the context
ncstats stashed_stats; // retain across a context reset, for closing banner
FILE* ttyfp; // FILE* for writing rasterized data
tinfo tcache; // terminfo cache
pthread_mutex_t pilelock; // guards pile list, locks resize in render
// desired margins (best-effort only), copied in from notcurses_options
int margin_t, margin_b, margin_r, margin_l;
int loglevel;
ncpalette palette; // 256-indexed palette can be used instead of/with RGB
bool palette_damage[NCPALETTESIZE];
bool touched_palette; // have we ever changed a palette entry?
uint64_t flags; // copied from notcurses_options
} notcurses;
typedef struct blitterargs {
// FIXME begy/begx are really only of interest to scaling; they ought be
// consumed there, and blitters ought always work with the scaled output.
int begy; // upper left start within visual
int begx;
int leny; // number of source pixels to use
int lenx;
uint64_t flags; // flags (as selected from ncvisual_options->flags)
uint32_t transcolor; // if non-zero, treat the lower 24 bits as a transparent color
union { // cell vs pixel-specific arguments
struct {
int placey; // placement within ncplane
int placex;
} cell; // for cells
struct {
int colorregs; // number of color registers
sprixel* spx; // sprixel object
int pxoffy; // pixel y-offset within origin cell
int pxoffx; // pixel x-offset within origin cell
int cellpxy; // targeted cell-pixel geometry. this ought come from the
int cellpxx; // target ncpile, or tcache in Direct Mode
} pixel; // for pixels
} u;
} blitterargs;
// scaledy and scaledx are output geometry from scaling; data is output data
// from scaling. we might actually need more pixels due to framing concerns,
// in which case just assume transparent input pixels where needed.
typedef int (*ncblitter)(struct ncplane* n, int linesize, const void* data,
int scaledy, int scaledx, const blitterargs* bargs);
// a system for rendering RGBA pixels as text glyphs or sixel/kitty bitmaps
struct blitset {
ncblitter_e geom;
unsigned width; // number of input pixels per output cell, width
unsigned height; // number of input pixels per output cell, height
// the EGCs which form the blitter. bits grow left to right, and then top to
// bottom. the first character is always a space, the last a full block.
const wchar_t* egcs;
// the EGCs which form the various levels of a given plotset. if the geometry
// is wide, things are arranged with the rightmost side increasing most
// quickly, i.e. it can be indexed as height arrays of 1 + height glyphs. i.e.
// the first five braille EGCs are all 0 on the left, [0..4] on the right.
const wchar_t* plotegcs;
ncblitter blit;
const char* name;
bool fill;
};
#include "blitset.h"
void reset_stats(ncstats* stats);
void summarize_stats(notcurses* nc);
void update_raster_stats(const struct timespec* time1, const struct timespec* time0, ncstats* stats);
void update_render_stats(const struct timespec* time1, const struct timespec* time0, ncstats* stats);
void update_raster_bytes(ncstats* stats, int bytes);
void update_write_stats(const struct timespec* time1, const struct timespec* time0, ncstats* stats, int bytes);
void sigwinch_handler(int signo);
void init_lang(void);
int reset_term_attributes(const tinfo* ti, fbuf* f);
int reset_term_palette(const tinfo* ti, fbuf* f, unsigned touchedpalette);
// if there were missing elements we wanted from terminfo, bitch about them here
void warn_terminfo(const notcurses* nc, const tinfo* ti);
int resize_callbacks_children(ncplane* n);
static inline ncpile*
ncplane_pile(const ncplane* n){
return n->pile;
}
static inline const ncpile*
ncplane_pile_const(const ncplane* n){
return n->pile;
}
static inline ncplane*
ncplane_stdplane(ncplane* n){
return notcurses_stdplane(ncplane_notcurses(n));
}
static inline const ncplane*
ncplane_stdplane_const(const ncplane* n){
return notcurses_stdplane_const(ncplane_notcurses_const(n));
}
// set the plane's widget and wdestruct fields, returning non-zero if they're
// already non-NULL (i.e. if the plane is already bound), unless we pass NULL
// (which ought be done from the widget destructor, to avoid corecursion).
static inline int
ncplane_set_widget(ncplane* n, void* w, void(*wdestruct)(void*)){
if(n->widget){
if(w){
logerror("plane is already bound to a widget\n");
return -1;
}
}else if(w == NULL){
return -1;
}
n->widget = w;
n->wdestruct = wdestruct;
return 0;
}
// initialize visualization backend (ffmpeg/oiio)
int ncvisual_init(int loglevel);
static inline int
fbcellidx(int row, int rowlen, int col){
//fprintf(stderr, "row: %d rowlen: %d col: %d\n", row, rowlen, col);
return row * rowlen + col;
}
// take a logical 'y' and convert it to the virtual 'y'. see HACKING.md.
static inline int
logical_to_virtual(const ncplane* n, int y){
//fprintf(stderr, "y: %d n->logrow: %d n->leny: %d\n", y, n->logrow, n->leny);
return (y + n->logrow) % n->leny;
}
int clear_and_home(notcurses* nc, tinfo* ti, fbuf* f);
static inline int
nfbcellidx(const ncplane* n, int row, int col){
return fbcellidx(logical_to_virtual(n, row), n->lenx, col);
}
// is the rgb value greyish? note that pure white and pure black are both
// considered greyish according to the definition of this function =].
static inline bool
rgb_greyish_p(unsigned r, unsigned g, unsigned b){
const unsigned GREYMASK = 0xf8;
if((r & GREYMASK) == (g & GREYMASK) && (g & GREYMASK) == (b & GREYMASK)){
return true;
}
return false;
}
// For our first attempt, O(1) uniform conversion from 8-bit r/g/b down 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.
static inline int
rgb_quantize_256(unsigned r, unsigned g, unsigned b){
// if all 5 MSBs match, return grey from 24-member grey ramp or pure
// black/white from original 16 (0 and 15, respectively)
if(rgb_greyish_p(r, g, b)){
// 8 and 238 come from https://terminalguide.namepad.de/attr/fgcol256/,
// which suggests 10-nit intervals otherwise.
if(r < 8){
return 0;
}else if(r > 238){
return 15;
}
return 232 + (r - 8) / 10;
}
r /= 43;
g /= 43;
b /= 43;
return r * 36 + g * 6 + b + 16;
}
// We get black, red, green, yellow, blue, magenta, cyan, and white. Ugh. Under
// an additive model: G + R -> Y, G + B -> C, R + B -> M, R + G + B -> W
static inline int
rgb_quantize_8(unsigned r, unsigned g, unsigned b){
static const int BLACK = 0;
static const int RED = 1;
static const int GREEN = 2;
static const int YELLOW = 3;
static const int BLUE = 4;
static const int MAGENTA = 5;
static const int CYAN = 6;
static const int WHITE = 7;
if(rgb_greyish_p(r, g, b)){
if(r < 64){
return BLACK;
}
return WHITE;
}
if(r < 128){ // we have no red
if(g < 128){ // we have no green
if(b < 128){
return BLACK;
}
return BLUE;
} // we have green
if(b < 128){
return GREEN;
}
return CYAN;
}else if(g < 128){ // we have red, but not green
if(b < 128){
return RED;
}
return MAGENTA;
}else if(b < 128){ // we have red and green
return YELLOW;
}
return WHITE;
}
// Given r, g, and b values 0..255, do a weighted average per Rec. 601, and
// return the 8-bit greyscale value (this value will be the r, g, and b value
// for the new color).
static inline int
rgb_greyscale(int r, int g, int b){
if(r < 0 || r > 255){
return -1;
}
if(g < 0 || g > 255){
return -1;
}
if(b < 0 || b > 255){
return -1;
}
// Use Rec. 601 scaling plus linear approximation of gamma decompression
float fg = (0.299 * (r / 255.0) + 0.587 * (g / 255.0) + 0.114 * (b / 255.0));
return fg * 255;
}
static inline const char*
pool_extended_gcluster(const egcpool* pool, const nccell* c){
if(cell_simple_p(c)){
return (const char*)&c->gcluster;
}
return egcpool_extended_gcluster(pool, c);
}
static inline nccell*
ncplane_cell_ref_yx(const ncplane* n, unsigned y, unsigned x){
return &n->fb[nfbcellidx(n, y, x)];
}
static inline void
cell_debug(const egcpool* p, const nccell* c){
fprintf(stderr, "gcluster: %08x %s style: 0x%04x chan: 0x%016" PRIx64 "\n",
c->gcluster, egcpool_extended_gcluster(p, c), c->stylemask,
c->channels);
}
static inline void
plane_debug(const ncplane* n, bool details){
unsigned dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
fprintf(stderr, "p: %p dim: %d/%d poolsize: %d\n", n, dimy, dimx, n->pool.poolsize);
if(details){
for(unsigned y = 0 ; y < 1 ; ++y){
for(unsigned x = 0 ; x < 10 ; ++x){
const nccell* c = &n->fb[fbcellidx(y, dimx, x)];
fprintf(stderr, "[%03d/%03d] ", y, x);
cell_debug(&n->pool, c);
}
}
}
}
static inline notcurses*
ncpile_notcurses(ncpile* p){
return p->nc;
}
static inline const notcurses*
ncpile_notcurses_const(const ncpile* p){
return p->nc;
}
static inline void
ncpile_debug(const ncpile* p, fbuf* f){
fbuf_printf(f, " ************************* %16p pile ****************************\n", p);
const ncplane* n = p->top;
const ncplane* prev = NULL;
int planeidx = 0;
while(n){
fbuf_printf(f, "%04d off y: %3d x: %3d geom y: %3u x: %3u curs y: %3u x: %3u %p %.4s\n",
planeidx, n->absy, n->absx, n->leny, n->lenx, n->y, n->x, n, n->name);
if(n->boundto || n->bnext || n->bprev || n->blist){
fbuf_printf(f, " bound %p ← %p → %p binds %p\n",
n->boundto, n->bprev, n->bnext, n->blist);
}
if(n->bprev && (*n->bprev != n)){
fbuf_printf(f, " WARNING: expected *->bprev %p, got %p\n", n, *n->bprev);
}
if(n->above != prev){
fbuf_printf(f, " WARNING: expected ->above %p, got %p\n", prev, n->above);
}
if(ncplane_pile_const(n) != p){
fbuf_printf(f, " WARNING: expected pile %p, got %p\n", p, ncplane_pile_const(n));
}
prev = n;
n = n->below;
++planeidx;
}
if(p->bottom != prev){
fbuf_printf(f, " WARNING: expected ->bottom %p, got %p\n", prev, p->bottom);
}
}
static inline void
notcurses_debug_fbuf(const notcurses* nc, fbuf* f){
const ncpile* p = ncplane_pile(nc->stdplane);
fbuf_printf(f, " -------------------------- notcurses debug state -----------------------------\n");
const ncpile* p0 = p;
do{
ncpile_debug(p0, f);
const ncpile* prev = p0;
p0 = p0->next;
if(p0->prev != prev){
fbuf_printf(f, "WARNING: expected ->prev %p, got %p\n", prev, p0->prev);
}
}while(p != p0);
fbuf_printf(f, " ______________________________________________________________________________\n");
}
// cell coordinates *within the sprixel*, not absolute
int sprite_wipe(const notcurses* nc, sprixel* s, int y, int x);
void sprixel_free(sprixel* s);
void sprixel_hide(sprixel* s);
// dimy and dimx are cell geometry, not pixel.
sprixel* sprixel_alloc(ncplane* n, int dimy, int dimx);
sprixel* sprixel_recycle(ncplane* n);
int sprite_init(const tinfo* t, int fd);
int sprite_clear_all(const tinfo* t, fbuf* f);
// these three all use absolute coordinates
void sprixel_invalidate(sprixel* s, int y, int x);
void sprixel_movefrom(sprixel* s, int y, int x);
void sprixel_debug(const sprixel* s, FILE* out);
void sixelmap_free(struct sixelmap *s);
// update any necessary cells underneath the sprixel pursuant to its removal.
// for sixel, this *achieves* the removal, and is performed on every cell.
// returns 1 if the graphic can be immediately freed (which is equivalent to
// asking whether it was sixel and there were no errors).
static inline int
sprite_scrub(const notcurses* n, const ncpile* p, sprixel* s){
//sprixel_debug(s, stderr);
logdebug("sprixel %u state %d\n", s->id, s->invalidated);
return n->tcache.pixel_scrub(p, s);
}
// precondition: s->invalidated is SPRIXEL_INVALIDATED or SPRIXEL_MOVED.
// returns -1 on error, or the number of bytes written.
static inline int
sprite_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
int yoff, int xoff){
if(!ti->pixel_draw){
return 0;
}
//sprixel_debug(s, stderr);
logdebug("sprixel %u state %d\n", s->id, s->invalidated);
return ti->pixel_draw(ti, p, s, f, yoff, xoff);
}
// precondition: s->invalidated is SPRIXEL_MOVED or SPRIXEL_INVALIDATED
// returns -1 on error, or the number of bytes written.
static inline int
sprite_redraw(notcurses* nc, const ncpile* p, sprixel* s, fbuf* f, int y, int x){
//sprixel_debug(s, stderr);
const tinfo* ti = &nc->tcache;
logdebug("sprixel %u state %d\n", s->id, s->invalidated);
if(s->invalidated == SPRIXEL_MOVED && ti->pixel_move){
// if we are kitty prior to 0.20.0, C=1 isn't available to us, and we must
// not emit it. we use sixel_maxy_pristine as a side channel to encode
// this version information.
bool noscroll = !ti->sixel_maxy_pristine;
return ti->pixel_move(s, f, noscroll, y, x);
}else{
if(!ti->pixel_draw){
return 0;
}
int r = ti->pixel_draw(ti, p, s, f, y, x);
// different terminals put the cursor at different places following
// emission of a bitmap graphic. just reset y/x.
nc->rstate.y = -1;
nc->rstate.x = -1;
return r;
}
}
// present a loaded graphic. only defined for kitty.
static inline int
sprite_commit(tinfo* ti, fbuf* f, sprixel* s, unsigned forcescroll){
if(ti->pixel_commit){
// if we are kitty prior to 0.20.0, C=1 isn't available to us, and we must
// not emit it. we use sixel_maxy_pristine as a side channel to encode
// this version information. direct mode, meanwhile, sets forcescroll.
bool noscroll = !ti->sixel_maxy_pristine && !forcescroll;
if(ti->pixel_commit(f, s, noscroll) < 0){
return -1;
}
}
return 0;
}
static inline void
cleanup_tam(tament* tam, int ydim, int xdim){
for(int y = 0 ; y < ydim ; ++y){
for(int x = 0 ; x < xdim ; ++x){
free(tam[y * xdim + x].auxvector);
tam[y * xdim + x].auxvector = NULL;
}
}
}
static inline void
destroy_tam(ncplane* p){
if(p->tam){
cleanup_tam(p->tam, p->leny, p->lenx);
free(p->tam);
p->tam = NULL;
}
}
static inline int
sprite_rebuild(const notcurses* nc, sprixel* s, int ycell, int xcell){
logdebug("rebuilding %d %d/%d\n", s->id, ycell, xcell);
const int idx = s->dimx * ycell + xcell;
int ret = 0;
// special case the transition back to SPRIXCELL_TRANSPARENT; this can be
// done in O(1), since the actual glyph needn't change.
if(s->n->tam[idx].state == SPRIXCELL_ANNIHILATED_TRANS){
s->n->tam[idx].state = SPRIXCELL_TRANSPARENT;
}else if(s->n->tam[idx].state == SPRIXCELL_ANNIHILATED){
uint8_t* auxvec = (uint8_t*)s->n->tam[idx].auxvector;
assert(auxvec);
// sets the new state itself
ret = nc->tcache.pixel_rebuild(s, ycell, xcell, auxvec);
if(ret > 0){
free(auxvec);
s->n->tam[idx].auxvector = NULL;
}
}else{
return 0;
}
// don't upset SPRIXEL_MOVED
if(s->invalidated == SPRIXEL_QUIESCENT){
if(s->n->tam[idx].state != SPRIXCELL_TRANSPARENT &&
s->n->tam[idx].state != SPRIXCELL_ANNIHILATED &&
s->n->tam[idx].state != SPRIXCELL_ANNIHILATED_TRANS){
s->invalidated = SPRIXEL_INVALIDATED;
}
}
return ret;
}
// |y| and |x| are scaled geometry on input, and clamped scaled geometry on
// output. |outy| is output geometry on output, and unused on input. output
// geometry is derived from scaled geometry and output requirements (that Sixel
// must be a multiple of six pixels tall). output width is always equal to
// scaled width. all are pixels.
// happy fact: common reported values for maximum sixel height are 256, 1024,
// and 4096...not a single goddamn one of which is divisible by six. augh.
static inline void
clamp_to_sixelmax(const tinfo* t, unsigned* y, unsigned* x, unsigned* outy, ncscale_e scaling){
if(t->sixel_maxy && *y > t->sixel_maxy){
*y = t->sixel_maxy;
}
*outy = *y;
if(*outy % t->sprixel_scale_height){
*outy += t->sprixel_scale_height - (*outy % t->sprixel_scale_height);
// FIXME use closed form
while(t->sixel_maxy && *outy > t->sixel_maxy){
*outy -= t->sprixel_scale_height;
}
if(scaling == NCSCALE_STRETCH || *y > *outy){
*y = *outy;
}
}
if(t->sixel_maxx && *x > t->sixel_maxx){
*x = t->sixel_maxx;
}
}
// any sprixcell which does not cover the entirety of the underlying cell
// cannot be SPRIXCELL_OPAQUE. this postprocesses the TAM, flipping any
// such sprixcells to SPRIXCELL_MIXED. |leny| and |lenx| are output geometry
// in pixels. |cdimy| and |cdimx| are output coverage in cells.
static inline void
scrub_tam_boundaries(tament* tam, int leny, int lenx, int cdimy, int cdimx){
// any sprixcells which don't cover the full cell underneath them cannot
// be SPRIXCELL_OPAQUE
const int cols = (lenx + cdimx - 1) / cdimx;
if(lenx % cdimx){
for(int y = 0 ; y < (leny + cdimy - 1) / cdimy ; ++y){
if(tam[y * cols + cols - 1].state == SPRIXCELL_OPAQUE_KITTY){
tam[y * cols + cols - 1].state = SPRIXCELL_MIXED_KITTY;
}else if(tam[y * cols + cols - 1].state == SPRIXCELL_OPAQUE_SIXEL){
tam[y * cols + cols - 1].state = SPRIXCELL_MIXED_SIXEL;
}
}
}
if(leny % cdimy){
const int y = (leny + cdimy - 1) / cdimy - 1;
for(int x = 0 ; x < cols ; ++x){
if(tam[y * cols + x].state == SPRIXCELL_OPAQUE_KITTY){
tam[y * cols + x].state = SPRIXCELL_MIXED_KITTY;
}else if(tam[y * cols + x].state == SPRIXCELL_OPAQUE_SIXEL){
tam[y * cols + x].state = SPRIXCELL_MIXED_SIXEL;
}
}
}
}
// get the TAM entry for these (absolute) coordinates
static inline sprixcell_e
sprixel_state(const sprixel* s, int y, int x){
const ncplane* stdn = notcurses_stdplane_const(ncplane_notcurses_const(s->n));
int localy = y - (s->n->absy - stdn->absy);
int localx = x - (s->n->absx - stdn->absx);
//fprintf(stderr, "TAM %d at %d/%d (%d/%d, %d/%d)\n", s->n->tam[localy * s->dimx + localx].state, localy, localx, y, x, s->dimy, s->dimx);
assert(localy >= 0);
assert(localy < (int)s->dimy);
assert(localx >= 0);
assert(localx < (int)s->dimx);
return s->n->tam[localy * s->dimx + localx].state;
}
static inline void
pool_release(egcpool* pool, nccell* c){
if(cell_extended_p(c)){
egcpool_release(pool, cell_egc_idx(c));
}
c->gcluster = 0; // don't subject ourselves to double-release problems
c->width = 0; // don't subject ourselves to geometric ambiguities
}
// set the nccell 'c' to point into the egcpool at location 'eoffset'
static inline void
set_gcluster_egc(nccell* c, int eoffset){
c->gcluster = htole(0x01000000ul) + htole(eoffset);
}
// Duplicate one nccell onto another, possibly crossing ncplanes.
static inline int
cell_duplicate_far(egcpool* tpool, nccell* targ, const ncplane* splane, const nccell* c){
pool_release(tpool, targ);
targ->stylemask = c->stylemask;
targ->channels = c->channels;
targ->width = c->width;
if(!cell_extended_p(c)){
targ->gcluster = c->gcluster;
return 0;
}
const char* egc = nccell_extended_gcluster(splane, c);
size_t ulen = strlen(egc);
int eoffset = egcpool_stash(tpool, egc, ulen);
if(eoffset < 0){
return -1;
}
set_gcluster_egc(targ, eoffset);
return 0;
}
int ncplane_resize_internal(ncplane* n, int keepy, int keepx,
unsigned keepleny, unsigned keeplenx,
int yoff, int xoff,
unsigned ylen, unsigned xlen);
int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache, int margin_b,
unsigned* cgeo_changed, unsigned* pgeo_changed)
__attribute__ ((nonnull (3, 5, 6)));
ALLOC static inline void*
memdup(const void* src, size_t len){
void* ret = malloc(len);
if(ret){
memcpy(ret, src, len);
}
return ret;
}
ALLOC void* bgra_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha);
ALLOC void* rgb_loose_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha);
ALLOC void* rgb_packed_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha);
// find the "center" cell of two lengths. in the case of even rows/columns, we
// place the center on the top/left. in such a case there will be one more
// cell to the bottom/right of the center.
static inline void
center_box(int* RESTRICT y, int* RESTRICT x){
if(y){
*y = (*y - 1) / 2;
}
if(x){
*x = (*x - 1) / 2;
}
}
// find the "center" cell of a plane. in the case of even rows/columns, we
// place the center on the top/left. in such a case there will be one more
// cell to the bottom/right of the center.
static inline void
ncplane_center(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
*y = n->leny;
*x = n->lenx;
center_box(y, x);
}
int ncvisual_bounding_box(const struct ncvisual* ncv, int* leny, int* lenx,
int* offy, int* offx);
// Our gradient is a 2d lerp among the four corners of the region. We start
// with the observation that each corner ought be its exact specified corner,
// and the middle ought be the exact average of all four corners' components.
// Another observation is that if all four corners are the same, every cell
// ought be the exact same color. From this arises the observation that a
// perimeter element is not affected by the other three sides:
//
// a corner element is defined by itself
// a perimeter element is defined by the two points on its side
// an internal element is defined by all four points
//
// 2D equation of state: solve for each quadrant's contribution (min 2x2):
//
// X' = (xlen - 1) - X
// Y' = (ylen - 1) - Y
// TLC: X' * Y' * TL
// TRC: X * Y' * TR
// BLC: X' * Y * BL
// BRC: X * Y * BR
// steps: (xlen - 1) * (ylen - 1) [maximum steps away from origin]
//
// Then add TLC + TRC + BLC + BRC + steps / 2, and divide by steps (the
// steps / 2 is to work around truncate-towards-zero).
static int
calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br,
unsigned y, unsigned x, unsigned ylen, unsigned xlen){
const int avm = (ylen - 1) - y;
const int ahm = (xlen - 1) - x;
if(xlen < 2){
if(ylen < 2){
return tl;
}
return (tl * avm + bl * y) / (ylen - 1);
}
if(ylen < 2){
return (tl * ahm + tr * x) / (xlen - 1);
}
const int tlc = ahm * avm * tl;
const int blc = ahm * y * bl;
const int trc = x * avm * tr;
const int brc = y * x * br;
const int divisor = (ylen - 1) * (xlen - 1);
return ((tlc + blc + trc + brc) + divisor / 2) / divisor;
}
// calculate one of the channels of a gradient at a particular point.
static inline uint32_t
calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr,
unsigned y, unsigned x, unsigned ylen, unsigned xlen){
uint32_t chan = 0;
ncchannel_set_rgb8_clipped(&chan,
calc_gradient_component(ncchannel_r(ul), ncchannel_r(ur),
ncchannel_r(ll), ncchannel_r(lr),
y, x, ylen, xlen),
calc_gradient_component(ncchannel_g(ul), ncchannel_g(ur),
ncchannel_g(ll), ncchannel_g(lr),
y, x, ylen, xlen),
calc_gradient_component(ncchannel_b(ul), ncchannel_b(ur),
ncchannel_b(ll), ncchannel_b(lr),
y, x, ylen, xlen));
ncchannel_set_alpha(&chan, ncchannel_alpha(ul)); // precondition: all αs are equal
return chan;
}
// calculate both channels of a gradient at a particular point, storing them
// into `channels'. x and y ought be the location within the gradient.
static inline void
calc_gradient_channels(uint64_t* channels, uint64_t ul, uint64_t ur,
uint64_t ll, uint64_t lr, unsigned y, unsigned x,
unsigned ylen, unsigned xlen){
if(!ncchannels_fg_default_p(ul)){
ncchannels_set_fchannel(channels,
calc_gradient_channel(ncchannels_fchannel(ul),
ncchannels_fchannel(ur),
ncchannels_fchannel(ll),
ncchannels_fchannel(lr),
y, x, ylen, xlen));
}else{
ncchannels_set_fg_default(channels);
}
if(!ncchannels_bg_default_p(ul)){
ncchannels_set_bchannel(channels,
calc_gradient_channel(ncchannels_bchannel(ul),
ncchannels_bchannel(ur),
ncchannels_bchannel(ll),
ncchannels_bchannel(lr),
y, x, ylen, xlen));
}else{
ncchannels_set_bg_default(channels);
}
}
// ncdirect needs to "fake" an isolated ncplane as a drawing surface for
// ncvisual_blit(), and thus calls these low-level internal functions.
// they are not for general use -- check ncplane_new() and ncplane_destroy().
ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* nopts);
void free_plane(ncplane* p);
// heap-allocated formatted output
ALLOC char* ncplane_vprintf_prep(const char* format, va_list ap);
// Resize the provided ncvisual to the specified 'rows' x 'cols', but do not
// change the internals of the ncvisual. Uses oframe.
int ncvisual_blit_internal(struct ncvisual* ncv, int rows, int cols,
ncplane* n, const struct blitset* bset,
const blitterargs* bargs);
static inline int
tty_emit(const char* seq, int fd){
if(!seq){
return -1;
}
size_t slen = strlen(seq);
if(blocking_write(fd, seq, slen)){
return -1;
}
return 0;
}
int set_fd_nonblocking(int fd, unsigned state, unsigned* oldstate);
static inline int
term_bg_palindex(const notcurses* nc, fbuf* f, unsigned pal){
const char* setab = get_escape(&nc->tcache, ESCAPE_SETAB);
if(setab){
return fbuf_emit(f, tiparm(setab, pal));
}
return 0;
}
static inline int
term_fg_palindex(const notcurses* nc, fbuf* f, unsigned pal){
const char* setaf = get_escape(&nc->tcache, ESCAPE_SETAF);
if(setaf){
return fbuf_emit(f, tiparm(setaf, pal));
}
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(fbuf* f, 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 = fbuf_emit(f, ton);
}
}else{
if(toff){ // how did this happen? we can turn it on, but not off?
ret = fbuf_emit(f, toff);
}
}
}
if(ret < 0){
return -1;
}
return 0;
}
// emit escapes such that the current style is equal to newstyle. if this
// required an sgr0 (which resets colors), normalized will be non-zero upon
// a successful return.
static inline int
coerce_styles(fbuf* f, const tinfo* ti, uint16_t* curstyle,
uint16_t newstyle, unsigned* normalized){
*normalized = 0; // we never currently use sgr0
int ret = 0;
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_BOLD,
get_escape(ti, ESCAPE_BOLD), get_escape(ti, ESCAPE_NOBOLD));
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_ITALIC,
get_escape(ti, ESCAPE_SITM), get_escape(ti, ESCAPE_RITM));
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_STRUCK,
get_escape(ti, ESCAPE_SMXX), get_escape(ti, ESCAPE_RMXX));
// underline and undercurl are exclusive. if we set one, don't go unsetting
// the other.
if(newstyle & NCSTYLE_UNDERLINE){ // turn on underline, or do nothing
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERLINE,
get_escape(ti, ESCAPE_SMUL), get_escape(ti, ESCAPE_RMUL));
}else if(newstyle & NCSTYLE_UNDERCURL){ // turn on undercurl, or do nothing
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERCURL,
get_escape(ti, ESCAPE_SMULX), get_escape(ti, ESCAPE_SMULNOX));
}else{ // turn off any underlining
ret |= term_setstyle(f, *curstyle, newstyle, NCSTYLE_UNDERCURL | NCSTYLE_UNDERLINE,
NULL, get_escape(ti, ESCAPE_RMUL));
}
*curstyle = newstyle;
return ret;
}
// DEC private mode set (DECSET) parameters (and corresponding XTerm resources)
#define SET_X10_MOUSE_PROT "9" // outdated, do not use, use x11
// we can combine 1000--1004, and then use 1005/1006/1015 for extended coordinates
#define SET_X11_MOUSE_PROT "1000" // for button events
#define SET_HILITE_MOUSE_PROT "1001" // for highlight tracking
#define SET_BTN_EVENT_MOUSE "1002" // for motion events with buttons
#define SET_ALL_EVENT_MOUSE "1003" // for motion events without buttons
#define SET_FOCUS_EVENT_MOUSE "1004" // for focus events
#define SET_UTF8_MOUSE_PROT "1005" // utf8-style extended coordinates
#define SET_SGR_MOUSE_PROT "1006" // sgr-style extended coordinates
#define SET_ALTERNATE_SCROLL "1007" // scroll in alternate screen (alternateScroll)
#define SET_TTYOUTPUT_SCROLL "1010" // scroll on tty output (scrollTtyOutput)
#define SET_KEYPRESS_SCROLL "1011" // scroll on keypress (scrollKey)
#define SET_URXVT_MOUSE_PROT "1015" // urxvt-style extended coordinates
#define SET_PIXEL_MOUSE_PROT "1016" // sgr-style, using pixles rather than cells
#define SET_ENABLE_ALTSCREEN "1046" // enable alternate screen (*sets* titeInhibit)
#define SET_ALTERNATE_SCREEN "1047" // replaces 47 (conflict w/DECGRPM) (titeInhibit)
#define SET_SAVE_CURSOR "1048" // save cursor ala DECSC (titeInhibit)
#define SET_SMCUP "1049" // 1047+1048 (titeInhibit)
// DECSET/DECRSTs can be chained with semicolons; can we generalize this? FIXME
#define DECSET(p) "\x1b[?" p "h"
#define DECRST(p) "\x1b[?" p "l"
int mouse_setup(tinfo* ti, unsigned eventmask);
// sync the drawing position to the specified location with as little overhead
// as possible (with nothing, if already at the right location). we prefer
// absolute horizontal moves (hpa) to relative ones, in the rare event that
// our understanding of our horizontal location is faulty. if we're moving from
// one plane to another, we emit an hpa no matter what.
// FIXME fall back to synthesized moves in the absence of capabilities (i.e.
// textronix lacks cup; fake it with horiz+vert moves)
// if hardcursorpos is non-zero, we always perform a cup. this is done when we
// don't know where the cursor currently is =].
static inline int
goto_location(notcurses* nc, fbuf* f, int y, int x, const ncplane* srcp){
//fprintf(stderr, "going to %d/%d from %d/%d hard: %u\n", y, x, nc->rstate.y, nc->rstate.x, nc->rstate.hardcursorpos);
int ret = 0;
// if we don't have hpa, force a cup even if we're only 1 char away. the only
// TERM i know supporting cup sans hpa is vt100, and vt100 can suck it.
// you can't use cuf for backwards moves anyway; again, vt100 can suck it.
const char* hpa = get_escape(&nc->tcache, ESCAPE_HPA);
if(nc->rstate.y == y && hpa){ // only need move x
if(nc->rstate.x == x){
if(nc->rstate.lastsrcp == srcp || !nc->tcache.gratuitous_hpa){
return 0; // needn't move shit
}
++nc->stats.s.hpa_gratuitous;
}
if(fbuf_emit(f, tiparm(hpa, x))){
return -1;
}
}else{
// cup is required, no need to verify existence
const char* cup = get_escape(&nc->tcache, ESCAPE_CUP);
if(fbuf_emit(f, tiparm(cup, y, x))){
return -1;
}
}
nc->rstate.x = x;
nc->rstate.y = y;
nc->rstate.lastsrcp = srcp;
return ret;
}
// how many edges need touch a corner for it to be printed?
static inline unsigned
box_corner_needs(unsigned ctlword){
return (ctlword & NCBOXCORNER_MASK) >> NCBOXCORNER_SHIFT;
}
// True if the cell does not generate background pixels (i.e., the cell is a
// solid or shaded block, or certain emoji).
static inline bool
nccell_nobackground_p(const nccell* c){
// needs to match all four bits of NC_NOBACKGROUND_MASK, not just one
return (c->channels & NC_NOBACKGROUND_MASK) == NC_NOBACKGROUND_MASK;
}
// True iff the foreground and background color are both RGB, and equal.
static inline bool
nccell_rgbequal_p(const nccell* c){
if(nccell_fg_default_p(c) || nccell_fg_palindex_p(c)){
return false;
}
if(nccell_bg_default_p(c) || nccell_bg_palindex_p(c)){
return false;
}
return nccell_fg_rgb(c) == nccell_bg_rgb(c);
}
// Returns a number 0 <= n <= 15 representing the four quadrants, and which (if
// any) are occupied due to blitting with a transparent background. The mapping
// is {tl, tr, bl, br}.
static inline unsigned
cell_blittedquadrants(const nccell* c){
return ((c->channels & 0x8000000000000000ull) ? 1 : 0) |
((c->channels & 0x0400000000000000ull) ? 2 : 0) |
((c->channels & 0x0200000000000000ull) ? 4 : 0) |
((c->channels & 0x0100000000000000ull) ? 8 : 0);
}
// Set this whenever blitting an ncvisual, when we have a transparent
// background. In such cases, ncvisuals underneath the cell must be rendered
// slightly differently.
static inline void
cell_set_blitquadrants(nccell* c, unsigned tl, unsigned tr, unsigned bl, unsigned br){
// FIXME want a static assert that these four constants OR together to
// equal CELL_BLITTERSTACK_MASK, bah
uint64_t newval = (tl ? 0x8000000000000000ull : 0) |
(tr ? 0x0400000000000000ull : 0) |
(bl ? 0x0200000000000000ull : 0) |
(br ? 0x0100000000000000ull : 0);
c->channels = ((c->channels & ~NC_BLITTERSTACK_MASK) | newval);
}
// Destroy a plane and all its bound descendants.
int ncplane_destroy_family(ncplane *ncp);
// Extract the 32-bit background channel from a cell.
static inline uint32_t
cell_bchannel(const nccell* cl){
return ncchannels_bchannel(cl->channels);
}
// Extract those elements of the channel which are common to both foreground
// and background channel representations.
static inline uint32_t
channel_common(uint32_t channel){
return channel & (NC_BGDEFAULT_MASK | NC_BG_RGB_MASK |
NC_BG_PALETTE | NC_BG_ALPHA_MASK);
}
// Extract those elements of the background channel which may be freely swapped
// with the foreground channel (alpha and coloring info).
static inline uint32_t
cell_bchannel_common(const nccell* cl){
return channel_common(cell_bchannel(cl));
}
// Extract the 32-bit foreground channel from a cell.
static inline uint32_t
cell_fchannel(const nccell* cl){
return ncchannels_fchannel(cl->channels);
}
// Extract those elements of the foreground channel which may be freely swapped
// with the background channel (alpha and coloring info).
static inline uint32_t
cell_fchannel_common(const nccell* cl){
return channel_common(cell_fchannel(cl));
}
// Set the 32-bit background channel of an nccell.
static inline uint64_t
cell_set_bchannel(nccell* cl, uint32_t channel){
return ncchannels_set_bchannel(&cl->channels, channel);
}
// Set the 32-bit foreground channel of an nccell.
static inline uint64_t
cell_set_fchannel(nccell* cl, uint32_t channel){
return ncchannels_set_fchannel(&cl->channels, channel);
}
// Returns the result of blending two channels. 'blends' indicates how heavily
// 'c1' ought be weighed. If 'blends' is 0, 'c1' will be entirely replaced by
// 'c2'. If 'c1' is otherwise the default color, 'c1' will not be touched,
// since we can't blend default colors. Likewise, if 'c2' is a default color,
// it will not be used (unless 'blends' is 0).
static inline unsigned
channels_blend(notcurses* nc, unsigned c1, unsigned c2, unsigned* blends){
if(ncchannel_alpha(c2) == NCALPHA_TRANSPARENT){
return c1; // do *not* increment *blends
}
bool c2default = ncchannel_default_p(c2);
bool c2palette = ncchannel_palindex_p(c2);
if(*blends == 0){
// don't just return c2, or you set wide status and all kinds of crap
if(c2default){
ncchannel_set_default(&c1);
}else if(c2palette){
ncchannel_set_palindex(&c1, ncchannel_palindex(c2));
}else{
ncchannel_set(&c1, c2 & NC_BG_RGB_MASK);
}
ncchannel_set_alpha(&c1, ncchannel_alpha(c2));
}else if(!c2default && !ncchannel_default_p(c1)){
unsigned rsum, gsum, bsum;
if(c2palette){
uint32_t rgb = nc->palette.chans[ncchannel_palindex(c2)];
bsum = rgb & 0xff;
gsum = (rgb >> 8u) & 0xff;
rsum = (rgb >> 16u) & 0xff;
}else{
ncchannel_rgb8(c2, &rsum, &gsum, &bsum);
}
rsum = (ncchannel_r(c1) * *blends + rsum) / (*blends + 1);
gsum = (ncchannel_g(c1) * *blends + gsum) / (*blends + 1);
bsum = (ncchannel_b(c1) * *blends + bsum) / (*blends + 1);
ncchannel_set_rgb8(&c1, rsum, gsum, bsum);
ncchannel_set_alpha(&c1, ncchannel_alpha(c2));
}
++*blends;
return c1;
}
// do not pass palette-indexed channels!
static inline uint64_t
cell_blend_fchannel(notcurses* nc, nccell* cl, unsigned channel, unsigned* blends){
return cell_set_fchannel(cl, channels_blend(nc, cell_fchannel(cl), channel, blends));
}
static inline uint64_t
cell_blend_bchannel(notcurses* nc, nccell* cl, unsigned channel, unsigned* blends){
return cell_set_bchannel(cl, channels_blend(nc, cell_bchannel(cl), channel, blends));
}
// a sprixel occupies the entirety of its associated plane, usually an entirely
// new, purpose-specific plane. |leny| and |lenx| are output geometry in pixels.
static inline int
plane_blit_sixel(sprixel* spx, fbuf* f, int leny, int lenx,
int parse_start, tament* tam, sprixel_e state){
if(sprixel_load(spx, f, leny, lenx, parse_start, state)){
return -1;
}
ncplane* n = spx->n;
if(n){
//fprintf(stderr, "TAM WAS: %p NOW: %p\n", n->tam, tam);
if(n->tam != tam){
destroy_tam(n);
}
n->tam = tam;
n->sprite = spx;
}
return 0;
}
// is it a control character? check C0 and C1, but don't count empty strings,
// nor single-byte strings containing only a NUL character.
static inline bool
is_control_egc(const unsigned char* egc, int bytes){
if(bytes == 1){
if(*egc && iscntrl(*egc)){
return true;
}
}else if(bytes == 2){
// 0xc2 followed by 0x80--0x9f are controls. 0xc2 followed by <0x80 is
// simply invalid utf8.
if(egc[0] == 0xc2){
if(egc[1] < 0xa0){
return true;
}
}
}
return false;
}
// lowest level of cell+pool setup. if the EGC changes the output to RTL, it
// must be suffixed with a LTR-forcing character by now. The four bits of
// NC_BLITTERSTACK_MASK ought already be initialized. If gcluster is four
// bytes or fewer, this function cannot fail.
static inline int
pool_blit_direct(egcpool* pool, nccell* c, const char* gcluster, int bytes, int cols){
pool_release(pool, c);
if(bytes < 0 || cols < 0){
return -1;
}
// we allow newlines to be blitted into the pool, as they're picked up and
// given special semantics by paint() and rasterization.
if(*gcluster != '\n' && is_control_egc((const unsigned char*)gcluster, bytes)){
logerror("not loading control character %u\n", *(const unsigned char*)gcluster);
return -1;
}
c->width = cols;
if(bytes <= 4){
c->gcluster = 0;
memcpy(&c->gcluster, gcluster, bytes);
}else{
int eoffset = egcpool_stash(pool, gcluster, bytes);
if(eoffset < 0){
return -1;
}
set_gcluster_egc(c, eoffset);
}
return bytes;
}
// Reset the quadrant occupancy bits, and pass the cell down to
// pool_blit_direct(). Returns the number of bytes loaded.
static inline int
pool_load_direct(egcpool* pool, nccell* c, const char* gcluster, int bytes, int cols){
c->channels &= ~NC_NOBACKGROUND_MASK;
return pool_blit_direct(pool, c, gcluster, bytes, cols);
}
static inline int
cell_load_direct(ncplane* n, nccell* c, const char* gcluster, int bytes, int cols){
return pool_load_direct(&n->pool, c, gcluster, bytes, cols);
}
// increment y by 1 and rotate the framebuffer up one line. x moves to 0.
void scroll_down(ncplane* n);
static inline bool
islinebreak(wchar_t wchar){
// UC_LINE_SEPARATOR + UC_PARAGRAPH_SEPARATOR
if(wchar == L'\n' || wchar == L'\v' || wchar == L'\f'){
return true;
}
const uint32_t mask = UC_CATEGORY_MASK_Zl | UC_CATEGORY_MASK_Zp;
return uc_is_general_category_withtable(wchar, mask);
}
static inline bool
iswordbreak(wchar_t wchar){
const uint32_t mask = UC_CATEGORY_MASK_Z |
UC_CATEGORY_MASK_Zs;
return uc_is_general_category_withtable(wchar, mask);
}
// the heart of damage detection. compare two nccells (from two different
// planes) for equality. if they are equal, return 0. otherwise, dup the second
// onto the first and return non-zero.
static inline int
cellcmp_and_dupfar(egcpool* dampool, nccell* damcell,
const ncplane* srcplane, const nccell* srccell){
if(damcell->stylemask == srccell->stylemask){
if(damcell->channels == srccell->channels){
const char* srcegc = nccell_extended_gcluster(srcplane, srccell);
const char* damegc = pool_extended_gcluster(dampool, damcell);
if(strcmp(damegc, srcegc) == 0){
return 0; // EGC match
}
}
}
cell_duplicate_far(dampool, damcell, srcplane, srccell);
return 1;
}
int get_tty_fd(FILE* ttyfp);
// Given the four channels arguments, verify that:
//
// - if any is default foreground, all are default foreground
// - if any is default background, all are default background
// - all foregrounds must have the same alpha
// - all backgrounds must have the same alpha
// - palette-indexed color must not be used
//
// If you only want to check n < 4 channels, just duplicate one.
bool check_gradient_args(uint64_t ul, uint64_t ur, uint64_t bl, uint64_t br);
// takes a signed starting coordinate (where -1 indicates the cursor's
// position), and an unsigned vector (where 0 indicates "everything
// remaining", i.e. to the right and below). returns 0 iff everything
// is valid and on the plane, filling in 'ystart'/'xstart' with the
// (non-negative) starting coordinates and 'ylen'/'xlen with the
// (positive) dimensions of the affected area.
static inline int
check_geometry_args(const ncplane* n, int y, int x,
unsigned* ylen, unsigned* xlen,
unsigned* ystart, unsigned* xstart){
// handle the special -1 case for y/x, and reject other negatives
if(y < 0){
if(y != -1){
logerror("invalid y: %d\n", y);
return -1;
}
y = n->y;
}
if(x < 0){
if(x != -1){
logerror("invalid x: %d\n", x);
return -1;
}
x = n->x;
}
// y and x are both now definitely positive, but might be off-plane.
// lock in y and x as ystart and xstart for unsigned comparisons.
*ystart = y;
*xstart = x;
unsigned ymax, xmax;
ncplane_dim_yx(n, &ymax, &xmax);
if(*ystart >= ymax || *xstart >= xmax){
logerror("invalid starting coordinates: %u/%u\n", *ystart, *xstart);
return -1;
}
// handle the special 0 case for ylen/xlen
if(*ylen == 0){
*ylen = ymax - *ystart;
}
if(*xlen == 0){
*xlen = xmax - *xstart;
}
// ensure ylen/xlen are on-plane
if(*ylen > ymax){
logerror("ylen > dimy %u > %u\n", *ylen, ymax);
return -1;
}
if(*xlen > xmax){
logerror("xlen > dimx %u > %u\n", *xlen, xmax);
return -1;
}
// ensure x + xlen and y + ylen are on-plane, without overflow
if(ymax - *ylen < *ystart){
logerror("y + ylen > ymax %u + %u > %u\n", *ystart, *ylen, ymax);
return -1;
}
if(xmax - *xlen < *xstart){
logerror("x + xlen > xmax %u + %u > %u\n", *xstart, *xlen, xmax);
return -1;
}
return 0;
}
void ncvisual_printbanner(fbuf* f);
// alpha comes to us 0--255, but we have only 3 alpha values to map them to
// (opaque, blended, and transparent). it's necessary that we display
// something for any non-zero alpha (see #1540), so the threshold is 1.
// we might want to map this to blended, but we only use opaque and
// transparent for now. if |transcolor| is non-zero, match its lower 24
// bits against each pixel's RGB value, and treat a match as transparent.
static inline bool
rgba_trans_p(uint32_t p, uint32_t transcolor){
if(ncpixel_a(p) < 192){
return true;
}
if(transcolor &&
(ncpixel_r(p) == (transcolor & 0xff0000ull) >> 16) &&
(ncpixel_g(p) == (transcolor & 0xff00ull) >> 8) &&
(ncpixel_b(p) == (transcolor & 0xffull))){
return true;
}
return false;
}
// get a non-negative "manhattan distance" between two rgb values
static inline uint32_t
rgb_diff(unsigned r1, unsigned g1, unsigned b1, unsigned r2, unsigned g2, unsigned b2){
uint32_t distance = 0;
distance += r1 > r2 ? r1 - r2 : r2 - r1;
distance += g1 > g2 ? g1 - g2 : g2 - g1;
distance += b1 > b2 ? b1 - b2 : b2 - b1;
//fprintf(stderr, "RGBDIFF %u %u %u %u %u %u: %u\n", r1, g1, b1, r2, g2, b2, distance);
return distance;
}
// returns non-zero iff the two planes intersect
static inline unsigned
ncplanes_intersect_p(const ncplane* p1, const ncplane* p2){
int y1, x1, y2, x2;
int b1, r1, b2, r2;
ncplane_abs_yx(p1, &y1, &x1);
b1 = y1 + ncplane_dim_y(p1) - 1;
r1 = x1 + ncplane_dim_x(p1) - 1;
ncplane_abs_yx(p2, &y2, &x2);
b2 = y2 + ncplane_dim_y(p2) - 1;
r2 = x2 + ncplane_dim_x(p2) - 1;
if(b1 < y2){ // p1 is above p2, no intersection
return 0;
}
if(b2 < y1){ // p2 is above p1, no intersection
return 0;
}
if(r1 < x2){ // p1 is to the left of p2, no intersection
return 0;
}
if(r2 < x1){ // p2 is to the left of p1, no intersection
return 0;
}
return 1;
}
static inline uint64_t
ncdirect_channels(const ncdirect* nc){
return nc->channels;
}
static inline bool
ncdirect_fg_default_p(const ncdirect* nc){
return ncchannels_fg_default_p(ncdirect_channels(nc));
}
static inline bool
ncdirect_bg_default_p(const ncdirect* nc){
return ncchannels_bg_default_p(ncdirect_channels(nc));
}
static inline bool
ncdirect_fg_palindex_p(const ncdirect* nc){
return ncchannels_fg_palindex_p(ncdirect_channels(nc));
}
static inline bool
ncdirect_bg_palindex_p(const ncdirect* nc){
return ncchannels_bg_palindex_p(ncdirect_channels(nc));
}
int ncdirect_set_fg_rgb_f(ncdirect* nc, unsigned rgb, fbuf* f);
int ncdirect_set_bg_rgb_f(ncdirect* nc, unsigned rgb, fbuf* f);
int term_fg_rgb8(const tinfo* ti, fbuf* f, unsigned r, unsigned g, unsigned b);
const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, bool may_degrade);
static inline int
rgba_blit_dispatch(ncplane* nc, const struct blitset* bset,
int linesize, const void* data,
int leny, int lenx, const blitterargs* bargs){
return bset->blit(nc, linesize, data, leny, lenx, bargs);
}
int ncvisual_geom_inner(const tinfo* ti, const struct ncvisual* n,
const struct ncvisual_options* vopts, ncvgeom* geom,
const struct blitset** bset,
unsigned* disppxy, unsigned* disppxx,
unsigned* outy, unsigned* outx,
int* placey, int* placex);
static inline const struct blitset*
rgba_blitter_low(const tinfo* tcache, ncscale_e scale, bool maydegrade,
ncblitter_e blitrec) {
if(blitrec == NCBLIT_DEFAULT){
blitrec = rgba_blitter_default(tcache, scale);
}
return lookup_blitset(tcache, blitrec, maydegrade);
}
// RGBA visuals all use NCBLIT_2x1 by default (or NCBLIT_1x1 if not in
// UTF-8 mode), but an alternative can be specified.
static inline const struct blitset*
rgba_blitter(const struct tinfo* tcache, const struct ncvisual_options* opts) {
const bool maydegrade = !(opts && (opts->flags & NCVISUAL_OPTION_NODEGRADE));
const ncscale_e scale = opts ? opts->scaling : NCSCALE_NONE;
return rgba_blitter_low(tcache, scale, maydegrade, opts ? opts->blitter : NCBLIT_DEFAULT);
}
// naive resize of |bmap| from |srows|x|scols| -> |drows|x|dcols|, suitable for
// pixel art. we either select at a constant interval (for shrinking) or duplicate
// at a constant ratio (for inflation). in the absence of a multimedia engine, this
// is the only kind of resizing we support.
static inline uint32_t*
resize_bitmap(const uint32_t* bmap, int srows, int scols, size_t sstride,
int drows, int dcols, size_t dstride){
if(sstride < scols * sizeof(*bmap)){
return NULL;
}
if(dstride < dcols * sizeof(*bmap)){
return NULL;
}
// FIXME if parameters match current setup, do nothing, and return bmap
size_t size = drows * dstride;
uint32_t* ret = (uint32_t*)malloc(size);
if(ret == NULL){
return NULL;
}
float xrat = (float)dcols / scols;
float yrat = (float)drows / srows;
int dy = 0;
for(int y = 0 ; y < srows ; ++y){
float ytarg = (y + 1) * yrat;
if(ytarg > drows){
ytarg = drows;
}
while(ytarg > dy){
int dx = 0;
for(int x = 0 ; x < scols ; ++x){
float xtarg = (x + 1) * xrat;
if(xtarg > dcols){
xtarg = dcols;
}
while(xtarg > dx){
ret[dy * dstride / sizeof(*ret) + dx] = bmap[y * sstride / sizeof(*ret) + x];
++dx;
}
}
++dy;
}
}
return ret;
}
// a neighbor on which to polyfill. by the time we get to it, it might or
// might not have been filled in. if so, discard immediately. otherwise,
// check self, and if valid, push all neighbors.
struct topolyfill {
int y, x;
struct topolyfill* next;
};
static inline struct topolyfill*
create_polyfill_op(int y, int x, struct topolyfill** stck){
// cast for the benefit of c++ callers
struct topolyfill* n = (struct topolyfill*)malloc(sizeof(*n));
if(n){
n->y = y;
n->x = x;
n->next = *stck;
*stck = n;
}
return n;
}
// implemented by a multimedia backend (ffmpeg or oiio), and installed
// prior to calling notcurses_core_init() (by notcurses_init()).
typedef struct ncvisual_implementation {
int (*visual_init)(int loglevel);
void (*visual_printbanner)(fbuf* f);
int (*visual_blit)(struct ncvisual* ncv, unsigned rows, unsigned cols, ncplane* n,
const struct blitset* bset, const blitterargs* barg);
struct ncvisual* (*visual_create)(void);
struct ncvisual* (*visual_from_file)(const char* fname);
// ncv constructors other than ncvisual_from_file() need to set up the
// AVFrame* 'frame' according to their own data, which is assumed to
// have been prepared already in 'ncv'.
void (*visual_details_seed)(struct ncvisual* ncv);
int (*visual_decode)(struct ncvisual* nc);
int (*visual_decode_loop)(struct ncvisual* nc);
int (*visual_stream)(notcurses* nc, struct ncvisual* ncv, float timescale,
ncstreamcb streamer, const struct ncvisual_options* vopts, void* curry);
ncplane* (*visual_subtitle)(ncplane* parent, const struct ncvisual* ncv);
int rowalign; // rowstride base, can be 0 for no padding
// do a persistent resize, changing the ncv itself
int (*visual_resize)(struct ncvisual* ncv, unsigned rows, unsigned cols);
void (*visual_destroy)(struct ncvisual* ncv);
bool canopen_images;
bool canopen_videos;
} ncvisual_implementation;
// populated by libnotcurses.so if linked with multimedia
API extern ncvisual_implementation* visual_implementation;
// prepend base with the Notcurses data directory as configured.
static inline char*
prefix_data(const char* base){
// need a byte for each of directory separator and nul terminator
const size_t dlen = strlen(NOTCURSES_SHARE);
size_t len = dlen + strlen(base) + 2;
char* path = (char*)malloc(len); // cast for C++ includers
if(path){
memcpy(path, NOTCURSES_SHARE, dlen);
path[dlen] = path_separator();
strcpy(path + dlen + 1, base);
}
return path;
}
// within unix, we can just use isatty(3). on windows, things work
// differently. for a true Windows Terminal, we'll have HANDLE pointers
// rather than file descriptors. in cygwin/msys2, isatty(3) always fails.
// so for __MINGW32__, always return true. otherwise return isatty(fd).
static inline int
tty_check(int fd){
#ifdef __MINGW32__
return _isatty(fd);
#endif
return isatty(fd);
}
// attempt to cancel the specified thread (not an error if we can't; it might
// have already exited), and then join it (an error here is propagated).
static inline int
cancel_and_join(const char* name, pthread_t tid, void** res){
if(pthread_cancel(tid)){
logerror("couldn't cancel %s thread\n", name); // tid might have died
}
if(pthread_join(tid, res)){
logerror("error joining %s thread\n", name);
return -1;
}
return 0;
}
static inline int
emit_scrolls(const tinfo* ti, int count, fbuf* f){
logdebug("emitting %d scrolls\n", count);
if(count > 1){
const char* indn = get_escape(ti, ESCAPE_INDN);
if(indn){
if(fbuf_emit(f, tiparm(indn, count)) < 0){
return -1;
}
return 0;
}
}
const char* ind = get_escape(ti, ESCAPE_IND);
if(ind == NULL){
ind = "\v";
}
// fell through if we had no indn
while(count > 0){
if(fbuf_emit(f, ind) < 0){
return -1;
}
--count;
}
return 0;
}
// both emit the |count > 0| scroll ops to |f|, and update the cursor
// tracking in |nc|
static inline int
emit_scrolls_track(notcurses* nc, int count, fbuf* f){
if(emit_scrolls(&nc->tcache, count, f)){
return -1;
}
nc->rstate.y -= count;
nc->rstate.x = 0;
return 0;
}
// replace or populate the TERM environment variable with 'termname'
int putenv_term(const char* termname) __attribute__ ((nonnull (1)));
// check environment for NOTCURSES_LOGLEVEL, and use it if defined.
int set_loglevel_from_env(ncloglevel_e* loglevel)
__attribute__ ((nonnull (1)));
#undef API
#undef ALLOC
#ifdef __cplusplus
}
#endif
#endif