ncplane_at_cursor (#76), CCCflag, nonblocking stdin (#78) (#84)

* put stdin into nonblocking mode, retry on short write to stdout #78
* wrap getc_blocking() around a poll #78
* get CCCflag from terminfo. stop clearing the screen in render/startup
* implement ncplane_at_cursor() #76
* ncplane_at_cursor() unit test for simples #76
* PlaneAtCursorComplex unit test #76
* PlaneAtCursorInsane() unit test #76
* nplane_at_cursor: return number of bytes, not just 0/-1
* uniblock-demo: add a bunch of pages from Unicode 12
* demo: make -d delay multiplier a float
* egcpool: check offset against poolsize in check_validity()
* notcurses_init(): set smkx/rmkx to NULL with pass_through_esc
* PlaneAtCursorAttrs unit test #76
* add ncplane_styles() accessor
This commit is contained in:
Nick Black 2019-11-30 22:53:24 -05:00 committed by GitHub
parent 0cb1c24622
commit a7d50b557d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 459 additions and 42 deletions

View File

@ -48,7 +48,7 @@ target_compile_options(notcurses
) )
target_compile_definitions(notcurses target_compile_definitions(notcurses
PUBLIC PUBLIC
_DEFAULT_SOURCE _XOPEN_SOURCE=700 _DEFAULT_SOURCE _XOPEN_SOURCE=700 _GNU_SOURCE
) )
file(GLOB DEMOSRCS CONFIGURE_DEPENDS src/demo/*.c) file(GLOB DEMOSRCS CONFIGURE_DEPENDS src/demo/*.c)

View File

@ -216,9 +216,9 @@ API int ncplane_move_below(struct ncplane* RESTRICT n, struct ncplane* RESTRICT
// Splice ncplane 'n' out of the z-buffer, and reinsert it above 'above'. // Splice ncplane 'n' out of the z-buffer, and reinsert it above 'above'.
API int ncplane_move_above(struct ncplane* RESTRICT n, struct ncplane* RESTRICT above); API int ncplane_move_above(struct ncplane* RESTRICT n, struct ncplane* RESTRICT above);
// Retrieve the topmost cell at the cursor location on the specified plane, // Retrieve the cell at the cursor location on the specified plane, returning
// returning it in 'c'. This copy is safe to use until the ncplane is destroyed. // it in 'c'. This copy is safe to use until the ncplane is destroyed/erased.
API void ncplane_at_cursor(const struct ncplane* n, cell* c); API int ncplane_at_cursor(struct ncplane* n, cell* c);
// Manipulate the opaque user pointer associated with this plane. // Manipulate the opaque user pointer associated with this plane.
// ncplane_set_userptr() returns the previous userptr after replacing // ncplane_set_userptr() returns the previous userptr after replacing
@ -321,6 +321,9 @@ API void ncplane_styles_on(struct ncplane* n, unsigned stylebits);
// Remove the specified styles from the ncplane's existing spec. // Remove the specified styles from the ncplane's existing spec.
API void ncplane_styles_off(struct ncplane* n, unsigned stylebits); API void ncplane_styles_off(struct ncplane* n, unsigned stylebits);
// Return the current styling for this ncplane.
API unsigned ncplane_styles(const struct ncplane* n);
// Fine details about terminal // Fine details about terminal
// Returns a 16-bit bitmask in the LSBs of supported curses-style attributes // Returns a 16-bit bitmask in the LSBs of supported curses-style attributes
@ -377,7 +380,7 @@ cell_styles_set(cell* c, unsigned stylebits){
// Get the style bits, shifted over into the LSBs. // Get the style bits, shifted over into the LSBs.
static inline unsigned static inline unsigned
cell_get_style(const cell* c){ cell_styles(const cell* c){
return (c->attrword & CELL_STYLE_MASK) >> 16u; return (c->attrword & CELL_STYLE_MASK) >> 16u;
} }
@ -531,6 +534,10 @@ cell_egc_idx(const cell* c){
return c->gcluster - 0x80; return c->gcluster - 0x80;
} }
// return a pointer to the NUL-terminated EGC referenced by 'c'. this pointer
// is invalidated by any further operation on the plane 'n', so...watch out!
API const char* cell_extended_gcluster(const struct ncplane* n, const cell* c);
// load up six cells with the EGCs necessary to draw a box. returns 0 on // load up six cells with the EGCs necessary to draw a box. returns 0 on
// success, -1 on error. on error, any cells this function might // success, -1 on error. on error, any cells this function might
// have loaded before the error are cell_release()d. There must be at least // have loaded before the error are cell_release()d. There must be at least
@ -592,6 +599,142 @@ API struct AVFrame* ncvisual_decode(struct ncvisual* nc);
// appropriate size. // appropriate size.
API int ncvisual_render(struct ncvisual* ncv, int ystop, int xstop); API int ncvisual_render(struct ncvisual* ncv, int ystop, int xstop);
// A panelreel is an notcurses region devoted to displaying zero or more
// line-oriented, contained panels between which the user may navigate. If at
// least one panel exists, there is an active panel. As much of the active
// panel as is possible is always displayed. If there is space left over, other
// panels are included in the display. Panels can come and go at any time, and
// can grow or shrink at any time.
//
// This structure is amenable to line- and page-based navigation via keystrokes,
// scrolling gestures, trackballs, scrollwheels, touchpads, and verbal commands.
enum bordermaskbits {
BORDERMASK_TOP = 0x1,
BORDERMASK_RIGHT = 0x2,
BORDERMASK_BOTTOM = 0x4,
BORDERMASK_LEFT = 0x8,
};
typedef struct panelreel_options {
// require this many rows and columns (including borders). otherwise, a
// message will be displayed stating that a larger terminal is necessary, and
// input will be queued. if 0, no minimum will be enforced. may not be
// negative. note that panelreel_create() does not return error if given a
// WINDOW smaller than these minima; it instead patiently waits for the
// screen to get bigger.
int min_supported_cols;
int min_supported_rows;
// use no more than this many rows and columns (including borders). may not be
// less than the corresponding minimum. 0 means no maximum.
int max_supported_cols;
int max_supported_rows;
// desired offsets within the surrounding WINDOW (top right bottom left) upon
// creation / resize. a panelreel_move() operation updates these.
int toff, roff, boff, loff;
// is scrolling infinite (can one move down or up forever, or is an end
// reached?). if true, 'circular' specifies how to handle the special case of
// an incompletely-filled reel.
bool infinitescroll;
// is navigation circular (does moving down from the last panel move to the
// first, and vice versa)? only meaningful when infinitescroll is true. if
// infinitescroll is false, this must be false.
bool circular;
// outcurses can draw a border around the panelreel, and also around the
// component tablets. inhibit borders by setting all valid bits in the masks.
// partially inhibit borders by setting individual bits in the masks. the
// appropriate attr and pair values will be used to style the borders.
// focused and non-focused tablets can have different styles. you can instead
// draw your own borders, or forgo borders entirely.
unsigned bordermask; // bitfield; 1s will not be drawn (see bordermaskbits)
uint32_t borderattr; // attributes used for panelreel border, no color!
int borderpair; // extended color pair for panelreel border
unsigned tabletmask; // bitfield; same as bordermask but for tablet borders
uint32_t tabletattr; // attributes used for tablet borders, no color!
int tabletpair; // extended color pair for tablet borders
uint32_t focusedattr;// attributes used for focused tablet borders, no color!
int focusedpair; // extended color pair for focused tablet borders
} panelreel_options;
struct tablet;
struct panelreel;
// Create a panelreel according to the provided specifications. Returns NULL on
// failure. w must be a valid WINDOW*, to which offsets are relative. Note that
// there might not be enough room for the specified offsets, in which case the
// panelreel will be clipped on the bottom and right. A minimum number of rows
// and columns can be enforced via popts. efd, if non-negative, is an eventfd
// that ought be written to whenever panelreel_touch() updates a tablet (this
// is useful in the case of nonblocking input).
struct panelreel* panelreel_create(struct ncplane* nc,
const panelreel_options* popts,
int efd);
// Tablet draw callback, provided a ncplane the first column that may be used,
// the first row that may be used, the first column that may not be used, the
// first row that may not be used, and a bool indicating whether output ought
// be clipped at the top (true) or bottom (false). Rows and columns are
// zero-indexed, and both are relative to the panel.
//
// Regarding clipping: it is possible that the tablet is only partially
// displayed on the screen. If so, it is either partially present on the top of
// the screen, or partially present at the bottom. In the former case, the top
// is clipped (cliptop will be true), and output ought start from the end. In
// the latter case, cliptop is false, and output ought start from the beginning.
//
// Returns the number of lines of output, which ought be less than or equal to
// maxy - begy, and non-negative (negative values might be used in the future).
typedef int (*tabletcb)(struct ncplane* p, int begx, int begy, int maxx,
int maxy, bool cliptop);
// Add a new tablet to the provided panelreel, having the callback object
// opaque. Neither, either, or both of after and before may be specified. If
// neither is specified, the new tablet can be added anywhere on the reel. If
// one or the other is specified, the tablet will be added before or after the
// specified tablet. If both are specifid, the tablet will be added to the
// resulting location, assuming it is valid (after->next == before->prev); if
// it is not valid, or there is any other error, NULL will be returned.
struct tablet* panelreel_add(struct panelreel* pr, struct tablet* after,
struct tablet *before, tabletcb cb, void* opaque);
// Return the number of tablets.
int panelreel_tabletcount(const struct panelreel* pr);
// Indicate that the specified tablet has been updated in a way that would
// change its display. This will trigger some non-negative number of callbacks
// (though not in the caller's context).
int panelreel_touch(struct panelreel* pr, struct tablet* t);
// Delete the tablet specified by t from the panelreel specified by pr. Returns
// -1 if the tablet cannot be found.
int panelreel_del(struct panelreel* pr, struct tablet* t);
// Delete the active tablet. Returns -1 if there are no tablets.
int panelreel_del_focused(struct panelreel* pr);
// Move to the specified location within the containing WINDOW.
int panelreel_move(struct panelreel* pr, int x, int y);
// Redraw the panelreel in its entirety, for instance after
// clearing the screen due to external corruption, or a SIGWINCH.
int panelreel_redraw(struct panelreel* pr);
// Return the focused tablet, if any tablets are present. This is not a copy;
// be careful to use it only for the duration of a critical section.
struct tablet* panelreel_focused(struct panelreel* pr);
// Change focus to the next tablet, if one exists
struct tablet* panelreel_next(struct panelreel* pr);
// Change focus to the previous tablet, if one exists
struct tablet* panelreel_prev(struct panelreel* pr);
// Destroy a panelreel allocated with panelreel_create(). Does not destroy the
// underlying WINDOW. Returns non-zero on failure.
int panelreel_destroy(struct panelreel* pr);
#undef API #undef API
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -8,6 +8,8 @@
#include <notcurses.h> #include <notcurses.h>
#include "demo.h" #include "demo.h"
static const unsigned long GIG = 1000000000;
struct timespec demodelay = { struct timespec demodelay = {
.tv_sec = 1, .tv_sec = 1,
.tv_nsec = 0, .tv_nsec = 0,
@ -19,7 +21,7 @@ usage(const char* exe, int status){
fprintf(out, "usage: %s [ -h ] [ -k ] [ -d ns ] [ -f renderfile ] demospec\n", exe); fprintf(out, "usage: %s [ -h ] [ -k ] [ -d ns ] [ -f renderfile ] demospec\n", exe);
fprintf(out, " -h: this message\n"); fprintf(out, " -h: this message\n");
fprintf(out, " -k: keep screen; do not switch to alternate\n"); fprintf(out, " -k: keep screen; do not switch to alternate\n");
fprintf(out, " -d: delay in nanoseconds between demos\n"); fprintf(out, " -d: delay multiplier (float)\n");
fprintf(out, " -f: render to file in addition to stdout\n"); fprintf(out, " -f: render to file in addition to stdout\n");
fprintf(out, "all demos are run if no specification is provided\n"); fprintf(out, "all demos are run if no specification is provided\n");
fprintf(out, " i: run intro\n"); fprintf(out, " i: run intro\n");
@ -173,13 +175,15 @@ handle_opts(int argc, char** argv, notcurses_options* opts){
} }
break; break;
case 'd':{ case 'd':{
char* eptr; float f;
unsigned long ns = strtoul(optarg, &eptr, 0); if(sscanf(optarg, "%f", &f) != 1){
if(*eptr){ fprintf(stderr, "Couldn't get a float from %s\n", optarg);
usage(*argv, EXIT_FAILURE); usage(*argv, EXIT_FAILURE);
} }
demodelay.tv_sec = ns / 1000000000; uint64_t ns = f * GIG;
demodelay.tv_nsec = ns % 1000000000; printf("F: %f NS: %lu\n", f, ns);
demodelay.tv_sec = ns / GIG;
demodelay.tv_nsec = ns % GIG;
break; break;
}default: }default:
usage(*argv, EXIT_FAILURE); usage(*argv, EXIT_FAILURE);

View File

@ -45,9 +45,39 @@ int unicodeblocks_demo(struct notcurses* nc){
{ .name = "Supplemental Mathematical Operators", .start = 0x2a00, }, { .name = "Supplemental Mathematical Operators", .start = 0x2a00, },
{ .name = "Glagolitic, Georgian Supplement, Tifinagh", .start = 0x2c00, }, { .name = "Glagolitic, Georgian Supplement, Tifinagh", .start = 0x2c00, },
{ .name = "Supplemental Punctuation, CJK Radicals", .start = 0x2e00, }, { .name = "Supplemental Punctuation, CJK Radicals", .start = 0x2e00, },
{ .name = "CJK Symbols and Punctuation", .start = 0x3000, },
{ .name = "Enclosed CJK Letters and Months", .start = 0x3200, },
{ .name = "CJK Unified Ideographs Extension A", .start = 0x3400, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x3600, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x3800, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x3a00, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x3c00, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x3e00, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4000, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4200, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4400, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4600, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4800, },
{ .name = "CJK Unified Ideographs Extension A (cont.)", .start = 0x4a00, },
{ .name = "CJK Unified Ideographs Extension A, Yijang Hexagram", .start = 0x4c00, },
{ .name = "CJK Unified Ideographs", .start = 0x4e00, },
{ .name = "Yi Syllables", .start = 0xa000, },
{ .name = "Yi Syllables", .start = 0xa200, },
{ .name = "Yi Syllables, Yi Radicals, Lisu, Vai", .start = 0xa400, },
{ .name = "Vai, Cyrillic Extended-B, Bamum, Tone Letters, Latin Extended-D", .start = 0xa600, },
{ .name = "Halfwidth and Fullwidth Forms", .start = 0xff00, },
{ .name = "Linear B Syllabary, Linear B Ideograms, Aegean Numbers, Phaistos Disc", .start = 0x10000, },
{ .name = "Lycian, Carian, Coptic Epact Numbers, Old Italic, Gothic, Old Permic", .start = 0x10200, },
{ .name = "Cuneiform", .start = 0x12000, }, { .name = "Cuneiform", .start = 0x12000, },
{ .name = "Cuneiform (cont.)", .start = 0x12200, }, { .name = "Cuneiform (cont.)", .start = 0x12200, },
{ .name = "Byzantine Musical Symbols, Musical Symbols", .start = 0x1d000, }, { .name = "Byzantine Musical Symbols, Musical Symbols", .start = 0x1d000, },
{ .name = "Ancient Greek Musical Notation, Mayan Numerals, Tai Xuan Jing, Counting Rods", .start = 0x1d200, },
{ .name = "Mathematical Alphanumeric Symbols", .start = 0x1d400, },
{ .name = "Mathematical Alphanumeric Symbols (cont.)", .start = 0x1d600, },
{ .name = "Sutton SignWriting", .start = 0x1d800, },
{ .name = "Glagolitic Supplement, Nyiakeng Puachue Hmong", .start = 0x1e000, },
{ .name = "Ottoman Siyaq Numbers", .start = 0x1ed00, },
{ .name = "Arabic Mathematical Alphabetic Symbols", .start = 0x1ee00, },
{ .name = "Mahjong Tiles, Domino Tiles, Playing Cards", .start = 0x1f000, }, { .name = "Mahjong Tiles, Domino Tiles, Playing Cards", .start = 0x1f000, },
{ .name = "Enclosed Ideographic Supplement, Miscellaneous Symbols", .start = 0x1f200, }, { .name = "Enclosed Ideographic Supplement, Miscellaneous Symbols", .start = 0x1f200, },
{ .name = "Miscellaneous Symbols and Pictographs (cont.)", .start = 0x1f400, }, { .name = "Miscellaneous Symbols and Pictographs (cont.)", .start = 0x1f400, },
@ -72,7 +102,6 @@ int unicodeblocks_demo(struct notcurses* nc){
nstotal /= 10; nstotal /= 10;
subdelay.tv_sec = nstotal / 1000000000; subdelay.tv_sec = nstotal / 1000000000;
subdelay.tv_nsec = nstotal % 1000000000; subdelay.tv_nsec = nstotal % 1000000000;
fprintf(stderr, "SUBDELAY: %lu %lu\n", subdelay.tv_sec, subdelay.tv_nsec);
for(sindex = 0 ; sindex < sizeof(blocks) / sizeof(*blocks) ; ++sindex){ for(sindex = 0 ; sindex < sizeof(blocks) / sizeof(*blocks) ; ++sindex){
uint32_t blockstart = blocks[sindex].start; uint32_t blockstart = blocks[sindex].start;
const char* description = blocks[sindex].name; const char* description = blocks[sindex].name;
@ -105,7 +134,6 @@ fprintf(stderr, "SUBDELAY: %lu %lu\n", subdelay.tv_sec, subdelay.tv_nsec);
memset(&ps, 0, sizeof(ps)); memset(&ps, 0, sizeof(ps));
wchar_t w = blockstart + chunk * CHUNKSIZE + z; wchar_t w = blockstart + chunk * CHUNKSIZE + z;
char utf8arr[MB_CUR_MAX + 1]; char utf8arr[MB_CUR_MAX + 1];
// FIXME we can print wide ones here, just add an extra line
if(wcwidth(w) >= 1 && iswprint(w)){ if(wcwidth(w) >= 1 && iswprint(w)){
int bwc = wcrtomb(utf8arr, w, &ps); int bwc = wcrtomb(utf8arr, w, &ps);
if(bwc < 0){ if(bwc < 0){
@ -122,8 +150,6 @@ fprintf(stderr, "SUBDELAY: %lu %lu\n", subdelay.tv_sec, subdelay.tv_nsec);
if(cell_load(n, &c, utf8arr) < 0){ // FIXME check full len was eaten? if(cell_load(n, &c, utf8arr) < 0){ // FIXME check full len was eaten?
return -1;; return -1;;
} }
/*ncplane_fg_rgb8(n, 0xad + z, 0xd8, 0xe6);
ncplane_bg_rgb8(n, 0x20, 0x20, 0x20);*/
cell_set_fg(&c, 0xad + z * 2, 0xd8, 0xe6 - z * 2); cell_set_fg(&c, 0xad + z * 2, 0xd8, 0xe6 - z * 2);
cell_set_bg(&c, 8 * chunk, 8 * chunk + z, 8 * chunk); cell_set_bg(&c, 8 * chunk, 8 * chunk + z, 8 * chunk);
if(ncplane_putc(n, &c) < 0){ if(ncplane_putc(n, &c) < 0){

View File

@ -179,6 +179,10 @@ egcpool_stash(egcpool* pool, const char* egc, size_t ulen){
// Run a consistency check on the offset; ensure it's a valid, non-empty EGC. // Run a consistency check on the offset; ensure it's a valid, non-empty EGC.
static inline bool static inline bool
egcpool_check_validity(const egcpool* pool, int offset){ egcpool_check_validity(const egcpool* pool, int offset){
if(offset >= pool->poolsize){
fprintf(stderr, "Offset (%d) greater than size (%d)\n", offset, pool->poolsize);
return false;
}
const char* egc = pool->pool + offset; const char* egc = pool->pool + offset;
if(*egc == '\0'){ if(*egc == '\0'){
fprintf(stderr, "Bad offset (%d): empty\n", offset); fprintf(stderr, "Bad offset (%d): empty\n", offset);

View File

@ -1,6 +1,7 @@
#include <ncurses.h> // needed for some definitions, see terminfo(3ncurses) #include <ncurses.h> // needed for some definitions, see terminfo(3ncurses)
#include <time.h> #include <time.h>
#include <term.h> #include <term.h>
#include <fcntl.h>
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
@ -8,6 +9,7 @@
#include <unistd.h> #include <unistd.h>
#include <stdlib.h> #include <stdlib.h>
#include <signal.h> #include <signal.h>
#include <sys/poll.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <libavutil/version.h> #include <libavutil/version.h>
@ -94,6 +96,7 @@ typedef struct notcurses {
struct termios tpreserved; // terminal state upon entry struct termios tpreserved; // terminal state upon entry
bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor
bool CCCflag; // terminfo-reported "CCC" flag for palette set capability
ncplane* top; // the contents of our topmost plane (initially entire screen) ncplane* top; // the contents of our topmost plane (initially entire screen)
ncplane* stdscr;// aliases some plane from the z-buffer, covers screen ncplane* stdscr;// aliases some plane from the z-buffer, covers screen
FILE* renderfp; // debugging FILE* to which renderings are written FILE* renderfp; // debugging FILE* to which renderings are written
@ -217,6 +220,26 @@ const void* ncplane_userptr_const(const ncplane* n){
return n->userptr; return n->userptr;
} }
// is the cursor in an invalid position? it never should be, but it's probably
// better to make sure (it's cheap) than to read from/write to random crap.
static bool
cursor_invalid_p(const ncplane* n){
if(n->y >= n->leny || n->x >= n->lenx){
return true;
}
if(n->y < 0 || n->x < 0){
return true;
}
return false;
}
int ncplane_at_cursor(ncplane* n, cell* c){
if(cursor_invalid_p(n)){
return -1;
}
return cell_duplicate(n, c, &n->fb[fbcellidx(n, n->y, n->x)]);
}
void ncplane_dim_yx(const ncplane* n, int* rows, int* cols){ void ncplane_dim_yx(const ncplane* n, int* rows, int* cols){
if(rows){ if(rows){
*rows = n->leny; *rows = n->leny;
@ -472,6 +495,8 @@ int ncplane_resize(ncplane* n, int keepy, int keepx, int keepleny,
return 0; return 0;
} }
// find the pointer on the z-index referencing the specified plane. writing to
// this pointer will remove the plane (and everything below it) from the stack.
static ncplane** static ncplane**
find_above_ncplane(ncplane* n){ find_above_ncplane(ncplane* n){
notcurses* nc = n->nc; notcurses* nc = n->nc;
@ -507,6 +532,7 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){
char* longname_term = longname(); char* longname_term = longname();
fprintf(stderr, "Term: %s\n", longname_term ? longname_term : "?"); fprintf(stderr, "Term: %s\n", longname_term ? longname_term : "?");
nc->RGBflag = tigetflag("RGB") == 1; nc->RGBflag = tigetflag("RGB") == 1;
nc->CCCflag = tigetflag("ccc") == 1;
if((nc->colors = tigetnum("colors")) <= 0){ if((nc->colors = tigetnum("colors")) <= 0){
fprintf(stderr, "This terminal doesn't appear to support colors\n"); fprintf(stderr, "This terminal doesn't appear to support colors\n");
nc->colors = 1; nc->colors = 1;
@ -578,6 +604,8 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){
if(!opts->pass_through_esc){ if(!opts->pass_through_esc){
term_verify_seq(&nc->smkx, "smkx"); term_verify_seq(&nc->smkx, "smkx");
term_verify_seq(&nc->rmkx, "rmkx"); term_verify_seq(&nc->rmkx, "rmkx");
}else{
nc->smkx = nc->rmkx = NULL;
} }
// Neither of these is supported on e.g. the "linux" virtual console. // Neither of these is supported on e.g. the "linux" virtual console.
if(!opts->inhibit_alternate_screen){ if(!opts->inhibit_alternate_screen){
@ -590,6 +618,19 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){
return 0; return 0;
} }
static int
make_nonblocking(FILE* fp){
int fd = fileno(fp);
if(fd < 0){
return -1;
}
int flags = fcntl(fd, F_GETFL, 0);
if(flags < 0){
return -1;
}
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
notcurses* notcurses_init(const notcurses_options* opts){ notcurses* notcurses_init(const notcurses_options* opts){
struct termios modtermios; struct termios modtermios;
notcurses* ret = malloc(sizeof(*ret)); notcurses* ret = malloc(sizeof(*ret));
@ -599,6 +640,10 @@ notcurses* notcurses_init(const notcurses_options* opts){
ret->ttyfp = opts->outfp; ret->ttyfp = opts->outfp;
ret->renderfp = opts->renderfp; ret->renderfp = opts->renderfp;
ret->ttyinfp = stdin; // FIXME ret->ttyinfp = stdin; // FIXME
if(make_nonblocking(ret->ttyinfp)){
free(ret);
return NULL;
}
if((ret->ttyfd = fileno(ret->ttyfp)) < 0){ if((ret->ttyfd = fileno(ret->ttyfp)) < 0){
fprintf(stderr, "No file descriptor was available in opts->outfp\n"); fprintf(stderr, "No file descriptor was available in opts->outfp\n");
free(ret); free(ret);
@ -644,7 +689,7 @@ notcurses* notcurses_init(const notcurses_options* opts){
free_plane(ret->top); free_plane(ret->top);
goto err; goto err;
} }
term_emit(ret->clear, ret->ttyfp, false); // term_emit(ret->clear, ret->ttyfp, false);
fprintf(ret->ttyfp, "\n" fprintf(ret->ttyfp, "\n"
" notcurses %s by nick black\n" " notcurses %s by nick black\n"
" terminfo from %s\n" " terminfo from %s\n"
@ -806,6 +851,10 @@ extended_gcluster(const ncplane* n, const cell* c){
return n->pool.pool + idx; 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 // 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 // number of columns occupied by this EGC (only an approximation; it's actually
// a property of the font being used). // a property of the font being used).
@ -835,18 +884,12 @@ term_putc(FILE* out, const ncplane* n, const cell* c){
static void static void
advance_cursor(ncplane* n, int cols){ advance_cursor(ncplane* n, int cols){
if(n->y >= n->leny){ if(cursor_invalid_p(n)){
if(n->x >= n->lenx){ return; // stuck!
return; // stuck!
}
} }
if((n->x += cols) >= n->lenx){ if((n->x += cols) >= n->lenx){
if(n->y >= n->leny){ ++n->y;
n->x = n->lenx; n->x -= n->lenx;
}else{
n->x -= n->lenx;
++n->y;
}
} }
} }
@ -882,7 +925,7 @@ term_setstyles(const notcurses* nc, FILE* out, uint32_t* curattr, const cell* c)
if(cell_inherits_style(c)){ if(cell_inherits_style(c)){
return 0; // change nothing return 0; // change nothing
} }
uint32_t cellattr = cell_get_style(c); uint32_t cellattr = cell_styles(c);
if(cellattr == *curattr){ if(cellattr == *curattr){
return 0; // happy agreement, change nothing return 0; // happy agreement, change nothing
} }
@ -973,6 +1016,36 @@ int ncplane_move_bottom(ncplane* n){
return 0; 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;
}
}while(written < buflen);
return 0;
}
// determine the best palette for the current frame, and write the necessary
// escape sequences to 'out'.
static int
prep_optimized_palette(notcurses* nc, FILE* out){
if(nc->RGBflag){
return 0; // DirectColor, no need to write palette
}
if(!nc->CCCflag){
return 0; // can't change palette
}
// FIXME
return 0;
}
// FIXME this needs to keep an invalidation bitmap, rather than blitting the // FIXME this needs to keep an invalidation bitmap, rather than blitting the
// world every time // world every time
int notcurses_render(notcurses* nc){ int notcurses_render(notcurses* nc){
@ -986,8 +1059,11 @@ int notcurses_render(notcurses* nc){
if(out == NULL){ if(out == NULL){
return -1; return -1;
} }
prep_optimized_palette(nc, out); // FIXME do what on failure?
uint32_t curattr = 0; // current attributes set (does not include colors) uint32_t curattr = 0; // current attributes set (does not include colors)
term_emit(nc->clear, out, false); // no need to write a clearscreen, since we update everything that's been
// changed. just move the physical cursor to the upper left corner.
term_emit(tiparm(nc->cup, 0, 0), out, false);
unsigned lastr, lastg, lastb; unsigned lastr, lastg, lastb;
unsigned lastbr, lastbg, lastbb; unsigned lastbr, lastbg, lastbb;
// we can elide a color escape iff the color has not changed between the two // we can elide a color escape iff the color has not changed between the two
@ -1060,8 +1136,8 @@ int notcurses_render(notcurses* nc){
} }
ret |= fclose(out); ret |= fclose(out);
fflush(nc->ttyfp); fflush(nc->ttyfp);
ssize_t w = write(nc->ttyfd, buf, buflen); fprintf(nc->ttyfp, "%s", buf);
if(w < 0 || (size_t)w != buflen){ if(blocking_write(nc->ttyfd, buf, buflen)){
ret = -1; ret = -1;
} }
/*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions, /*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions,
@ -1119,7 +1195,7 @@ int cell_duplicate(ncplane* n, cell* targ, const cell* c){
} }
int ncplane_putc(ncplane* n, const cell* c){ int ncplane_putc(ncplane* n, const cell* c){
if(n->y >= n->leny || n->x >= n->lenx){ if(cursor_invalid_p(n)){
return -1; return -1;
} }
cell* targ = &n->fb[fbcellidx(n, n->y, n->x)]; cell* targ = &n->fb[fbcellidx(n, n->y, n->x)];
@ -1230,6 +1306,10 @@ void ncplane_styles_set(ncplane* n, unsigned stylebits){
((stylebits & 0xffff) << 16u); ((stylebits & 0xffff) << 16u);
} }
unsigned ncplane_styles(const ncplane* n){
return (n->attrword & CELL_STYLE_MASK) >> 16u;
}
int ncplane_printf(ncplane* n, const char* format, ...){ int ncplane_printf(ncplane* n, const char* format, ...){
int ret; int ret;
va_list va; va_list va;
@ -1365,6 +1445,7 @@ handle_getc(const notcurses* nc __attribute__ ((unused)), cell* c, int kpress,
if(kpress == 0x04){ // ctrl-d if(kpress == 0x04){ // ctrl-d
return -1; return -1;
} }
// FIXME look for keypad
if(kpress < 0x80){ if(kpress < 0x80){
c->gcluster = kpress; c->gcluster = kpress;
}else{ }else{
@ -1387,18 +1468,31 @@ int notcurses_getc(const notcurses* nc, cell* c, ncspecial_key* special){
return handle_getc(nc, c, r, special); return handle_getc(nc, c, r, special);
} }
// we set our infd to non-blocking on entry, so to do a blocking call (without
// burning cpu) we'll need to set up a poll().
int notcurses_getc_blocking(const notcurses* nc, cell* c, ncspecial_key* special){ int notcurses_getc_blocking(const notcurses* nc, cell* c, ncspecial_key* special){
int r = getc(nc->ttyinfp); struct pollfd pfd = {
if(r < 0){ .fd = fileno(nc->ttyinfp),
if(errno == EINTR){ .events = POLLIN | POLLRDHUP,
if(resize_seen){ .revents = 0,
resize_seen = 0; };
c->gcluster = 0; int pret;
*special = NCKEY_RESIZE; while((pret = poll(&pfd, 1, -1)) >= 0){
return 1; if(pret == 0){
} continue;
}
int r = getc(nc->ttyinfp);
if(r < 0){
if(errno == EINTR){
if(resize_seen){
resize_seen = 0;
c->gcluster = 0;
*special = NCKEY_RESIZE;
return 1;
}
}
return handle_getc(nc, c, r, special);
} }
return r;
} }
return handle_getc(nc, c, r, special); return -1;
} }

View File

@ -1,3 +1,4 @@
#include <cstdlib>
#include <notcurses.h> #include <notcurses.h>
#include "main.h" #include "main.h"
@ -394,3 +395,148 @@ TEST_F(NcplaneTest, GrowPlane) {
// FIXME check dims, pos // FIXME check dims, pos
ASSERT_EQ(0, ncplane_destroy(newp)); ASSERT_EQ(0, ncplane_destroy(newp));
} }
// we ought be able to see what we're about to render, or have just rendered, or
// in any case whatever's in the virtual framebuffer for a plane
TEST_F(NcplaneTest, PlaneAtCursorSimples){
const char STR1[] = "Jackdaws love my big sphinx of quartz";
const char STR2[] = "Cwm fjord bank glyphs vext quiz";
const char STR3[] = "Pack my box with five dozen liquor jugs";
ncplane_styles_set(n_, 0);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
ASSERT_LT(0, ncplane_putstr(n_, STR1));
int dimy, dimx;
ncplane_dim_yx(n_, &dimy, &dimx);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - strlen(STR2)));
ASSERT_LT(0, ncplane_putstr(n_, STR2));
int y, x;
ncplane_cursor_yx(n_, &y, &x);
ASSERT_EQ(2, y);
ASSERT_EQ(0, x);
ASSERT_LT(0, ncplane_putstr(n_, STR3));
cell testcell = CELL_TRIVIAL_INITIALIZER;
ASSERT_EQ(0, ncplane_at_cursor(n_, &testcell)); // want nothing at the cursor
EXPECT_EQ(0, testcell.gcluster);
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR1
EXPECT_EQ(STR1[0], testcell.gcluster);
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - 1));
ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want last char of STR2
EXPECT_EQ(STR2[strlen(STR2) - 1], testcell.gcluster);
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
// FIXME maybe check all cells?
EXPECT_EQ(0, notcurses_render(nc_));
}
// ensure we read back what's expected for latinesque complex characters
TEST_F(NcplaneTest, PlaneAtCursorComplex){
const char STR1[] = "Σιβυλλα τι θελεις; respondebat illa:";
const char STR2[] = "αποθανειν θελω";
const char STR3[] = "Война и мир"; // just thrown in to complicate things
ncplane_styles_set(n_, 0);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
ASSERT_LT(0, ncplane_putstr(n_, STR1));
int dimy, dimx;
ncplane_dim_yx(n_, &dimy, &dimx);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - mbstowcs(NULL, STR2, 0)));
ASSERT_LT(0, ncplane_putstr(n_, STR2));
int y, x;
ncplane_cursor_yx(n_, &y, &x);
ASSERT_EQ(2, y);
ASSERT_EQ(0, x);
ASSERT_LT(0, ncplane_putstr(n_, STR3));
cell testcell = CELL_TRIVIAL_INITIALIZER;
ncplane_at_cursor(n_, &testcell); // should be nothing at the cursor
EXPECT_EQ(0, testcell.gcluster);
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR1
EXPECT_STREQ("Σ", cell_extended_gcluster(n_, &testcell));
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - mbstowcs(NULL, STR2, 0)));
ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR2
EXPECT_STREQ("α", cell_extended_gcluster(n_, &testcell));
EXPECT_EQ(0, testcell.attrword);
EXPECT_EQ(0, testcell.channels);
// FIXME maybe check all cells?
EXPECT_EQ(0, notcurses_render(nc_));
}
// half-width, double-width, huge-yet-narrow, all that crap
TEST_F(NcplaneTest, PlaneAtCursorInsane){
const char EGC0[] = "\uffe0"; // fullwidth cent sign ¢
const char EGC1[] = "\u00c5"; // neutral A with ring above Å
const char EGC2[] = "\u20a9"; // half-width won ₩
const char EGC3[] = "\u212b"; // ambiguous angstrom Å
const char EGC4[] = "\ufdfd"; // neutral yet huge bismillah ﷽
std::array<cell, 5> tcells;
for(auto i = 0u ; i < tcells.size() ; ++i){
cell_init(&tcells[i]);
}
ASSERT_LT(1, cell_load(n_, &tcells[0], EGC0));
ASSERT_LT(1, cell_load(n_, &tcells[1], EGC1));
ASSERT_LT(1, cell_load(n_, &tcells[2], EGC2));
ASSERT_LT(1, cell_load(n_, &tcells[3], EGC3));
ASSERT_LT(1, cell_load(n_, &tcells[4], EGC4));
for(auto i = 0u ; i < tcells.size() ; ++i){
ASSERT_LT(1, ncplane_putc(n_, &tcells[i]));
}
EXPECT_EQ(0, notcurses_render(nc_));
int x = 0;
for(auto i = 0u ; i < tcells.size() ; ++i){
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, x));
cell testcell = CELL_TRIVIAL_INITIALIZER;
ASSERT_LT(0, ncplane_at_cursor(n_, &testcell));
EXPECT_STREQ(cell_extended_gcluster(n_, &tcells[i]),
cell_extended_gcluster(n_, &testcell));
EXPECT_EQ(0, testcell.attrword);
wchar_t w;
ASSERT_LT(0, mbtowc(&w, cell_extended_gcluster(n_, &tcells[i]), MB_CUR_MAX));
if(wcwidth(w) == 2){
EXPECT_EQ(CELL_WIDEASIAN_MASK, testcell.channels);
++x;
}else{
EXPECT_EQ(0, testcell.channels);
}
++x;
}
}
// test that we read back correct attrs/colors despite changing defaults
TEST_F(NcplaneTest, PlaneAtCursorAttrs){
const char STR1[] = "this has been a world destroyer production";
const char STR2[] = "not to mention dank";
const char STR3[] = "da chronic lives";
ncplane_styles_set(n_, CELL_STYLE_BOLD);
ASSERT_LT(0, ncplane_putstr(n_, STR1));
int y, x;
ncplane_cursor_yx(n_, &y, &x);
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y + 1, x - strlen(STR2)));
ncplane_styles_on(n_, CELL_STYLE_ITALIC);
ASSERT_LT(0, ncplane_putstr(n_, STR2));
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y + 2, x - strlen(STR3)));
ncplane_styles_off(n_, CELL_STYLE_BOLD);
ASSERT_LT(0, ncplane_putstr(n_, STR3));
ncplane_styles_off(n_, CELL_STYLE_ITALIC);
EXPECT_EQ(0, notcurses_render(nc_));
int newx;
ncplane_cursor_yx(n_, &y, &newx);
EXPECT_EQ(newx, x);
cell testcell = CELL_TRIVIAL_INITIALIZER;
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 2, x - 1));
ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
EXPECT_EQ(testcell.gcluster, STR1[strlen(STR1) - 1]);
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 1, x - 1));
ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
EXPECT_EQ(testcell.gcluster, STR2[strlen(STR2) - 1]);
EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y, x - 1));
ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
EXPECT_EQ(testcell.gcluster, STR3[strlen(STR3) - 1]);
}