documentation and declarations (#13)

ncplane: flesh out API
ncplane: line and erase APIs
README.md: document differences from ncurses
notcurses_init(): print long term name
CMake: link librt into notcurses
cell: move functionality out to header
This commit is contained in:
Nick Black 2019-11-23 09:05:32 -05:00 committed by GitHub
parent a8721df75a
commit 7e92c8bb82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 270 additions and 86 deletions

View File

@ -12,6 +12,7 @@ include(GNUInstallDirs)
find_package(PkgConfig REQUIRED)
pkg_check_modules(TERMINFO REQUIRED tinfo>=6.1)
find_library(LIBRT rt)
file(GLOB LIBSRCS CONFIGURE_DEPENDS src/lib/*.c)
add_library(notcurses SHARED ${LIBSRCS})
@ -22,8 +23,9 @@ target_include_directories(notcurses
"${TERMINFO_INCLUDE_DIR}"
)
target_link_libraries(notcurses
INTERFACE
"${TERMINFO_LIBRARIES}"
PRIVATE
"${TERMINFO_LIBRARIES}"
"${LIBRT}"
)
set_target_properties(notcurses PROPERTIES
PUBLIC_HEADER "include/notcurses.h"

View File

@ -25,7 +25,9 @@ complex TUIs*. I would argue that teletypes etc. are fundamentally unsuitable.
Most operating systems seem reasonable targets, but I only have Linux and
FreeBSD available for testing.
notcurses makes use of the Terminfo library shipped with NCURSES.
notcurses makes use of the Terminfo library shipped with NCURSES. notcurses
uses Terminfo wherever possible, benefiting greatly from its portability and
thoroughness.
notcurses opens up advanced functionality for the interactive user on
workstations, phones, laptops, and tablets, at the expense of e.g.
@ -40,11 +42,16 @@ Why use this non-standard library?
24-bit RGB color.
* Visual features not directly available via NCURSES, including images,
fonts, and video.
fonts, video, high-contrast text, and transparent regions.
* Thread safety, and use in parallel programs, has been a design consideration
from the beginning.
* It's Apache2-licensed in its entirety, as opposed to the
[drama in several acts](https://invisible-island.net/ncurses/ncurses-license.html])
that is the NCURSES license (the latter is [summarized](https://invisible-island.net/ncurses/ncurses-license.html#issues_freer)
as "a restatement of MIT-X11").
On the other hand, if you're targeting industrial or critical applications,
or wish to benefit from the time-tested reliability and portability of Curses,
you should by all means use that fine library.
@ -127,3 +134,45 @@ from a lower `ncplane` from being seen. An `ncplane` corresponds loosely to an
[NCURSES Panel](https://invisible-island.net/ncurses/ncurses-intro.html#panels),
but is the primary drawing surface of notcurses—there is no object
corresponding to a bare NCURSES `WINDOW`.
## Differences from NCURSES
The biggest difference, of course, is that notcurses is not an implementation
of X/Open (aka XSI) Curses, nor part of SUS4-2018.
The detailed differences between notcurses and NCURSES probably can't be fully
enumerated, and if they could, no one would want to read it. With that said,
some design decisions might surprise NCURSES programmers:
* The screen is not cleared on entry.
* There is no distinct `PANEL` type. The z-buffer is a fundamental property,
and all drawable surfaces are ordered along the z axis. There is no
equivalent to `update_panels()`.
* Scrolling is disabled by default, and cannot be globally enabled.
* The hardware cursor is disabled by default, when supported (`civis` capability).
* Echoing of input is disabled by default, and `cbreak` mode is used by default.
* Colors are always specified as 24 bits in 3 components (RGB). If necessary,
these will be quantized for the actual terminal. There are no "color pairs".
* There is no distinct "pad" concept (these are NCURSES `WINDOW`s created with
the `newpad()` function). All drawable surfaces can exceed the display size.
* Multiple threads can freely call into notcurses, so long as they're not
accessing the same data. In particular, it is always safe to concurrently
mutate different ncplanes in different threads.
* NCURSES has thread-ignorant and thread-semi-safe versions, trace-enabled and
traceless versions, and versions with and without support for wide characters.
notcurses is one library: no tracing, wide characters, thread safety.
### Features missing relative to NCURSES
This isn't "features currently missing", but rather "features I do not intend
to implement".
* There is no immediate-output mode (`immedok()`, `echochar()` etc.)
With that said, `ncplane_putc()` followed by `notcurses_render()` ought
be just as fast as `echochar()`.
* There is no support for soft labels (`slk_init()`, etc.).
* There is no concept of subwindows which share memory with their parents.
* There is no tracing functionality ala `trace(3NCURSES)`. Superior external
tracing solutions exist, such as `bpftrace`.
* There is no timeout functionality for input (`timeout()`, `halfdelay()`, etc.).
Roll your own with any of the four thousand ways to do it.

View File

@ -1,17 +1,68 @@
#ifndef NOTCURSES_NOTCURSES
#define NOTCURSES_NOTCURSES
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// Get a human-readable string describing the running ncurses version.
// Get a human-readable string describing the running notcurses version.
const char* notcurses_version(void);
struct ncplane; // a drawable notcurses surface
struct notcurses; // notcurses state for a given terminal
struct cell; // a coordinate on an ncplane: wchar_t(s) and styling
struct ncplane; // a drawable notcurses surface, composed of cells
struct notcurses; // notcurses state for a given terminal, composed of ncplanes
// A cell corresponds to a single character cell on some plane. At any cell, we
// can have a short array of wchar_t (L'\0'-terminated; we need support an
// array due to the possibility of combining characters), a foreground color,
// a background color, and an attribute set. The rules on the wchar_t array are
// the same as those for an ncurses 6.1 cchar_t:
//
// FIXME i don't care for this (large) static array one whit. we're not bound
// to X/Open, and owe cchar_t no fealty. i do like the attrs and colors being
// bound up with it, though. this definition is almost certain to change. we
// could overload some invalid UTF-8 construction (say a first byte greater
// than 0x7f) to escape out to some attached storage pool, using the
// difference as an index into the pool. we would have 25 bits, after all...
//
// * At most one spacing character, which must be the first if present.
// * Up to NCCHARW_MAX-1 nonspacing characters follow. Extra spacing
// characters are ignored. A nonspacing character is one for which wcwidth()
// returns zero, and is not the wide NUL (L'\0').
// * A single control character can be present, with no other characters (save
// an immediate wide NUL (L'\0').
// * If there are fewer than NCCHARW_MAX wide characters, they must be
// terminated with a wide NUL (L'\0').
//
// Multi-column characters can only have a single attribute/color.
// https://pubs.opengroup.org/onlinepubs/007908799/xcurses/intov.html
//
// Each cell occupies 16 bytes (128 bits). The surface is thus ~2MB for a
// (pretty large) 500x200 terminal. At 80x43, it's less than 100KB.
#define NCCHARW_MAX 1
typedef struct cell {
wchar_t cchar[NCCHARW_MAX]; // 1 * 4b -> 4b
// The classic NCURSES WA_* attributes (16 bits), plus 16 bits of alpha.
uint32_t attrword; // + 4b -> 8b
// (channels & 0x8000000000000000ull): inherit styling from prior cell
// (channels & 0x4000000000000000ull): foreground is *not* "default color"
// (channels & 0x3f00000000000000ull): reserved, must be 0
// (channels & 0x00ffffff00000000ull): foreground in 3x8 RGB (rrggbb)
// (channels & 0x0000000080000000ull): in the middle of a multicolumn glyph
// (channels & 0x0000000040000000ull): background is *not* "default color"
// (channels & 0x000000003f000000ull): reserved, must be 0
// (channels & 0x0000000000ffffffull): background in 3x8 RGB (rrggbb)
// At render time, these 24-bit values are quantized down to terminal
// capabilities, if necessary. There's a clear path to 10-bit support should
// we one day need it, but keep things cagey for now. "default color" is
// best explained by color(3NCURSES). ours is the same concept. until the
// "not default color" bit is set, any color you load will be ignored.
uint64_t channels; // + 8b == 16b
} cell;
// FIXME we'll need to expose this definition for ncplane_getwc()
struct cell; // the contents of a single cell on a single plane
@ -98,6 +149,11 @@ void ncplane_yx(const struct ncplane* n, int* y, int* x);
// Splice ncplane 'n' out of the z-buffer, and reinsert it above 'above'.
void ncplane_move_above(struct ncplane* n, struct ncplane* above);
// Splice ncplane 'n' out of the z-buffer, and reinsert it below 'below'.
void ncplane_move_below(struct ncplane* n, struct ncplane* below);
// Splice ncplane 'n' out of the z-buffer, and reinsert it at the top or bottom.
void ncplane_move_top(struct ncplane* n);
void ncplane_move_bottom(struct ncplane* n);
// Splice ncplane 'n' out of the z-buffer, and reinsert it below 'below'.
void ncplane_move_below(struct ncplane* n, struct ncplane* below);
@ -106,10 +162,38 @@ void ncplane_move_below(struct ncplane* n, struct ncplane* below);
void ncplane_move_top(struct ncplane* n);
void ncplane_move_bottom(struct ncplane* n);
// Set the current cell in the specified plane to the provided wchar_t array.
// The array must not be more than one column worth of wchar_t's, among other
// restrictions. Advances the cursor by one cell.
int ncplane_putwc(struct ncplane* n, const wchar_t* wcs);
// Replace the cell underneath the cursor with the provided cell 'c', and
// advance the cursor by one cell *unless we are at the end of the plane*.
// On success, returns 1 if the cursor was advanced, and 0 otherwise. On
// failure, -1 is returned.
int ncplane_putwc(struct ncplane* n, const cell* c);
// Retrieve the cell under the cursor, returning it in 'c'.
void ncplane_getwc(const struct ncplane* n, cell* c);
// Write a series of wchar_ts to the current location. They will be interpreted
// as a series of columns (according to the definition of ncplane_putwc()).
// Advances the cursor by some positive number of cells; this number is returned
// on success. On error, a non-positive number is returned, indicating the
// number of cells which were written before the error.
int ncplane_putwstr(struct ncplane* n, const wchar_t* wstr);
// The ncplane equivalent of wprintf(3) and vwprintf(3), themselves the
// wide-character equivalents of printf(3) and vprintf(3).
int ncplane_wprintf(struct ncplane* n, const wchar_t* format, ...);
// Draw horizontal or vertical lines using the specified cell of wchar_t's,
// starting at the current cursor position. The cursor will end at the cell
// following the last cell output (even, perhaps counter-intuitively, when
// drawing vertical lines), just as if ncplane_putwc() was called at that spot.
// Returns the number of cells drawn on success. On error, returns the negative
// number of cells drawn.
int ncplane_hline(struct ncplane* n, const wchar_t* wcs, int len);
int ncplane_vline(struct ncplane* n, int yoff, const wchar_t* wcs, int len);
// Erase all content in the ncplane, resetting all attributes to normal, all
// colors to -1, and all cells to undrawn.
void ncplane_erase(struct ncplane* n);
// Retrieve the cell under the cursor, returning it in 'c'.
void ncplane_getwc(const struct ncplane* n, struct cell* c);
@ -134,6 +218,63 @@ int ncplane_wprintf(struct ncplane* n, const wchar_t* format, ...);
int ncplane_fg_rgb8(struct ncplane* n, int r, int g, int b);
int ncplane_bg_rgb8(struct ncplane* n, int r, int g, int b);
// Fine details about terminal
// FIXME bools for the various WA_* attributes
// FIXME verify that they are both supported AND not part of no_color_video
// Returns the number of colors supported by the palette, or 0 if there is no
// palette (DirectColor or no colors).
int notcurses_palette_size(const struct notcurses* nc);
// Working with cells
// Copies as many wchar_ts out of 'wstr' and into 'c' as it can, according to
// the rules of cell composition. If the leading part of wstr is not a valid
// cell, -1 is returned. Returns the number of wchar_ts copied, not including
// the terminating L'\0' (if 'wstr' is empty, zero is returned).
int load_cell(cell* c, const wchar_t* wstr);
static inline uint32_t
cell_fg_rgb(uint64_t channel){
return (channel & 0x00ffffff00000000ull) >> 32u;
}
static inline uint32_t
cell_bg_rgb(uint64_t channel){
return (channel & 0xffffffull);
}
static inline unsigned
cell_rgb_red(uint32_t rgb){
return (rgb & 0xff0000ull) >> 16u;
}
static inline unsigned
cell_rgb_green(uint32_t rgb){
return (rgb & 0xff00ull) >> 8u;
}
static inline unsigned
cell_rgb_blue(uint32_t rgb){
return (rgb & 0xffull);
}
static inline void
cell_set_fg(cell* c, unsigned r, unsigned g, unsigned b){
uint64_t rgb = (r & 0xffull) << 48u;
rgb |= (g & 0xffull) << 40u;
rgb |= (b & 0xffull) << 32u;
c->channels = (c->channels & 0x00ffffff00000000ull) | rgb;
}
static inline void
cell_get_fb(const cell* c, unsigned* r, unsigned* g, unsigned* b){
*r = cell_rgb_red(cell_fg_rgb(c->channels));
*g = cell_rgb_green(cell_fg_rgb(c->channels));
*b = cell_rgb_blue(cell_fg_rgb(c->channels));
}
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -24,17 +24,19 @@ int main(void){
fprintf(stderr, "Couldn't get standard plane\n");
goto err;
}
int x, cols;
ncplane_dimyx(ncp, NULL, &cols);
if(ncplane_cursor_move_yx(ncp, 1, 1)){
goto err;
}
for(x = 1 ; x < cols - 1 ; ++x){
if(ncplane_fg_rgb8(ncp, 200, 0, 200)){
int x, y, rows, cols;
ncplane_dimyx(ncp, &rows, &cols);
cell c;
load_cell(&c, /*L"💣*/L"X");
cell_set_fg(&c, 200, 0, 200);
for(y = 1 ; y < rows - 1 ; ++y){
if(ncplane_cursor_move_yx(ncp, y, 1)){
goto err;
}
if(ncplane_putwc(ncp, L"X"/*💣*/)){
goto err;
for(x = 1 ; x < cols - 1 ; ++x){
if(ncplane_putwc(ncp, &c)){
goto err;
}
}
}
if(notcurses_render(nc)){

View File

@ -1,4 +1,5 @@
#include <ncurses.h> // needed for some definitions, see terminfo(3ncurses)
#include <time.h>
#include <term.h>
#include <errno.h>
#include <stdio.h>
@ -9,34 +10,6 @@
#include "notcurses.h"
#include "version.h"
// A cell represents a single character cell in the display. At any cell, we
// can have a short array of wchar_t (L'\0'-terminated; we need support an
// array due to the possibility of combining characters), a foreground color,
// a background color, and an attribute set. The rules on the wchar_t array are
// the same as those for an ncurses 6.1 cchar_t:
//
// * At most one spacing character, which must be the first if present.
// * Up to CCHARW_MAX-1 nonspacing characters follow. Extra spacing characters
// are ignored. A nonspacing character is one for which wcwidth() returns
// zero, and is not the wide NUL (L'\0').
// * A single control character can be present, with no other characters (save
// an immediate wide NUL (L'\0').
// * If there are fewer than CCHARW_MAX wide characters, they must be
// terminated with a wide NUL (L'\0').
//
// Multi-column characters can only have a single attribute/color.
// https://pubs.opengroup.org/onlinepubs/007908799/xcurses/intov.html
//
// Each cell occupies 32 bytes (256 bits). The surface is thus ~4MB for a
// (pretty large) 500x200 terminal. At 80x43, it's less than 200KB.
typedef struct cell {
wchar_t cchar[CCHARW_MAX + 1]; // 6 * 4b == 24b
// The attrword covers classic NCURSES attributes (16 bits), plus foreground
// and background color, stored as 3x8bits of RGB. At render time, these
// 24-bit values are quantized down to terminal capabilities, if necessary.
uint64_t attrs;
} cell;
// Some capabilities are so fundamental that we don't attempt to run without
// them. Essentially, we require a two-dimensional, random-access terminal.
static const char* required_caps[] = {
@ -71,6 +44,7 @@ typedef struct ncplane {
typedef struct notcurses {
int ttyfd; // file descriptor for controlling tty (takes stdin)
timer_t timer; // CLOCK_MONOTONIC timer for benchmarking
int colors; // number of colors usable for this screen
// We verify that some capabilities exist (see required_caps). Those needn't
// be checked before further use; just use tiparm() directly. These might be
@ -268,10 +242,16 @@ notcurses* notcurses_init(const notcurses_options* opts){
if(ret == NULL){
return ret;
}
if(timer_create(CLOCK_MONOTONIC, NULL, &ret->timer)){
fprintf(stderr, "Error initializing monotonic clock (%s)\n", strerror(errno));
free(ret);
return NULL;
}
ret->ttyfd = opts->outfd;
if(tcgetattr(ret->ttyfd, &ret->tpreserved)){
fprintf(stderr, "Couldn't preserve terminal state for %d (%s)\n",
ret->ttyfd, strerror(errno));
timer_delete(ret->timer);
free(ret);
return NULL;
}
@ -287,6 +267,8 @@ notcurses* notcurses_init(const notcurses_options* opts){
fprintf(stderr, "Terminfo error %d (see terminfo(3ncurses))\n", termerr);
goto err;
}
char* longname_term = longname();
fprintf(stderr, "Term: %s\n", longname_term ? longname_term : "?");
ret->RGBflag = tigetflag("RGB") == 1;
if((ret->colors = tigetnum("colors")) <= 0){
fprintf(stderr, "This terminal doesn't appear to support colors\n");
@ -331,6 +313,7 @@ notcurses* notcurses_init(const notcurses_options* opts){
err:
tcsetattr(ret->ttyfd, TCSANOW, &ret->tpreserved);
timer_delete(ret->timer);
free(ret);
return NULL;
}
@ -364,26 +347,6 @@ erpchar(int c){
return EOF;
}
#define CELL_RMASK 0x0000ff0000000000ull
#define CELL_GMASK 0x000000ff00000000ull
#define CELL_BMASK 0x00000000ff000000ull
#define CELL_RGBMASK (CELL_RMASK | CELL_GMASK | CELL_BMASK)
static void
cell_set_fg(cell* c, unsigned r, unsigned g, unsigned b){
uint64_t rgb = (r & 0xffull) << 40u;
rgb += (g & 0xffull) << 32u;
rgb += (b & 0xffull) << 24u;
c->attrs = (c->attrs & ~CELL_RGBMASK) | rgb;
}
static void
cell_get_fb(const cell* c, unsigned* r, unsigned* g, unsigned* b){
*r = (c->attrs & CELL_RMASK) >> 40u;
*g = (c->attrs & CELL_GMASK) >> 32;
*b = (c->attrs & CELL_BMASK) >> 24u;
}
int ncplane_fg_rgb8(ncplane* n, int r, int g, int b){
if(r >= 256 || g >= 256 || b >= 256){
return -1;
@ -451,12 +414,13 @@ term_movyx(int y, int x){
}
// Write the cchar (one cell's worth of wchar_t's) to the physical terminal
// FIXME probably want to use a wmemstream
static int
term_putw(const notcurses* nc, const cell* c){
ssize_t w;
size_t len = wcslen(c->cchar);
size_t len = wcsnlen(c->cchar, sizeof(c->cchar) / sizeof(*c->cchar));
if(len == 0){
if((w = write(nc->ttyfd, " ", 1)) < 0 || (size_t)w != 1){
if((w = write(nc->ttyfd, " ", 1)) < 0 || (size_t)w != 1){ // FIXME
return -1;
}
return 0;
@ -518,15 +482,6 @@ void ncplane_cursor_yx(const ncplane* n, int* y, int* x){
}
}
static inline bool
validate_wchar_cell(const cell* c, const wchar_t* wcs){
if(wcslen(wcs) >= sizeof(c->cchar) / sizeof(*c->cchar)){
return false;
}
// FIXME check other crap
return true;
}
static void
advance_cursor(struct ncplane* n){
if(++n->x == n->lenx){
@ -537,12 +492,44 @@ advance_cursor(struct ncplane* n){
}
}
int ncplane_putwc(struct ncplane* n, const wchar_t* wcs){
cell* c = &n->fb[fbcellidx(n, n->y, n->x)];
if(!validate_wchar_cell(c, wcs)){
return -1;
}
memcpy(c->cchar, wcs, wcslen(wcs) * sizeof(*wcs));
int ncplane_putwc(struct ncplane* n, const cell* c){
cell* targ = &n->fb[fbcellidx(n, n->y, n->x)];
memcpy(targ, c, sizeof(*c));
advance_cursor(n);
return 0;
}
int load_cell(cell* c, const wchar_t* wstr){
int copied = 0;
do{
if(copied == sizeof(c->cchar) / sizeof(*c->cchar)){
if(!wcwidth(*wstr)){ // next one *must* be a spacing char
return -1; // filled up the buffer
}
break; // no terminator on cells which fill the array [shrug]
}
if(copied && *wstr != L'\0' && wcwidth(*wstr)){
break; // only nonspacing (zero-width) chars after first; throw it back
}
c->cchar[copied++] = *wstr;
}while(*wstr++ != L'\0'); // did we just copy L'\0'? if so, we're always done
return copied;
}
int ncplane_putwstr(struct ncplane* n, const wchar_t* wstr){
int ret = 0;
// FIXME speed up this blissfully naive solution
cell c;
while(*wstr != L'\0'){
int wcs = load_cell(&c, wstr);
if(wcs <= 0){
return -ret;
}
wstr += wcs;
if(ncplane_putwc(n, &c)){
return -ret;
}
++ret;
}
return ret;
}

View File

@ -97,12 +97,15 @@ TEST_F(NcplaneTest, RejectBadRGB) {
EXPECT_NE(0, ncplane_fg_rgb8(n_, 255, 256, 255));
EXPECT_NE(0, ncplane_fg_rgb8(n_, 255, 255, 256));
EXPECT_NE(0, ncplane_fg_rgb8(n_, 256, 256, 256));
EXPECT_EQ(0, ncplane_fg_rgb8(n_, 255, 255, 255));
}
// Verify we can emit a wide character, and it advances the cursor
TEST_F(NcplaneTest, EmitWchar) {
wchar_t cchar[] = L"";
EXPECT_EQ(0, ncplane_putwc(n_, cchar));
cell c;
load_cell(&c, cchar);
EXPECT_EQ(0, ncplane_putwc(n_, &c));
int x, y;
ncplane_cursor_yx(n_, &y, &x);
EXPECT_EQ(y, 0);