debian | ||
doc | ||
include | ||
src | ||
tests | ||
tools | ||
.drone.yml | ||
.gitignore | ||
CMakeLists.txt | ||
LICENSE | ||
README.md |
notcurses
cleanroom TUI library for modern terminal emulators. definitely not curses.
Introduction
-
What it is: a library facilitating complex TUIs on modern terminal emulators, supporting vivid colors and Unicode to the maximum degree possible. Many tasks delegated to Curses can be achieved using notcurses (and vice versa).
-
What it is not: a source-compatible X/Open Curses implementation, nor a replacement for NCURSES on existing systems, nor a widely-ported and -tested bedrock of Open Source, nor a battle-proven, veteran library.
notcurses abandons the X/Open Curses API bundled as part of the Single UNIX Specification. The latter shows its age, and seems not capable of making use of terminal functionality such as unindexed 24-bit color ("DirectColor", not to be confused with 8-bit indexed 24-bit color, aka "TrueColor" or (by NCURSES) as "extended color"). For some necessary background, consult Thomas E. Dickey's superb and authoritative NCURSES FAQ. As such, notcurses is not a drop-in Curses replacement. It is almost certainly less portable, and definitely tested on less hardware. Sorry about that. Ultimately, I hope to properly support all terminals supporting the features necessary for 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.
Whenever possible, notcurses makes use of the Terminfo library shipped with NCURSES, 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. industrial and retail terminals (or even the Linux virtual console, which offers only eight colors and limited glyphs).
Why use this non-standard library?
-
A svelter design than that codified in X/Open. All exported identifiers are prefixed to avoid namespace collisions. Far fewer identifiers are exported overall. All APIs natively suport UTF-8, and the
cell
API is based around Unicode's Extended Grapheme Cluster concept. -
Visual features not directly available via NCURSES, including images, fonts, video, high-contrast text, and transparent regions. All APIs natively support 24-bit color, quantized down as necessary for the terminal.
-
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 that is the NCURSES license (the latter is summarized 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.
Requirements
- A C11 and a C++14 compiler
- CMake 3.13.0+
- NCurses 6.1+
- From FFMpeg: libswscale 5.0+, libavformat 57.0+, libavutil 56.0+
Use
A program wishing to use notcurses will need to link it, ideally using the
output of pkg-config --libs notcurses
. It is advised to compile with the
output of pkg-config --cflags notcurses
. If using CMake, a support file is
provided, and can be accessed as notcurses
.
Before calling into notcurses—and usually as one of the first calls of the
program—be sure to call setlocale(3)
with an appropriate UTF-8 LC_ALL
locale. It is usually appropriate to pass NULL
to setlocale()
, relying on
the user to properly set the LANG
environment variable.
notcurses requires an available terminfo(5)
definition appropriate for the
terminal. It is usually appropriate to pass NULL
in the termtype
field of a
notcurses_options
struct, relying on the user to properly set the TERM
environment variable. This variable is usually set by the terminal itself. It
might be necessary to manually select a higher-quality definition for your
terminal, i.e. xterm-direct
as opposed to xterm
or xterm-256color
.
Each terminal can be prepared via a call to notcurses_init()
, which is
supplied a struct of type notcurses_options
:
// Get a human-readable string describing the running notcurses version.
const char* notcurses_version(void);
struct cell; // a coordinate on an ncplane: an EGC plus styling
struct ncplane; // a drawable notcurses surface, composed of cells
struct notcurses; // notcurses state for a given terminal, composed of ncplanes
// Configuration for notcurses_init().
typedef struct notcurses_options {
// The name of the terminfo database entry describing this terminal. If NULL,
// the environment variable TERM is used. Failure to open the terminal
// definition will result in failure to initialize notcurses.
const char* termtype;
// An open FILE* for this terminal, on which we will generate output. If
// not attached to a sufficiently capable terminal, notcurses will refuse
// to start. You'll usually want stdout.
FILE* outfp;
// If smcup/rmcup capabilities are indicated, notcurses defaults to making
// use of the "alternate screen". This flag inhibits use of smcup/rmcup.
bool inhibit_alternate_screen;
// By default, we hide the cursor if possible. This flag inhibits use of
// the civis capability, retaining the cursor.
bool retain_cursor;
// We typically install a signal handler for SIGINT and SIGQUIT that restores
// the screen, and then calls the old signal handler. Set this to inhibit
// registration of any signal handlers.
bool no_quit_sighandlers;
// We typically install a signal handler for SIGWINCH that generates a resize
// event in the notcurses_getc() queue. Set this to inhibit the handler.
bool no_winch_sighandler;
} notcurses_options;
// Initialize a notcurses context, corresponding to a connected terminal.
// Returns NULL on error, including any failure to initialize terminfo.
struct notcurses* notcurses_init(const notcurses_options* opts);
// Destroy a notcurses context.
int notcurses_stop(struct notcurses* nc);
notcurses_stop
should be called before exiting your program to restore the
terminal settings and free resources.
The vast majority of the notcurses API draws into virtual buffers. Only upon
a call to notcurses_render
will the visible terminal display be updated to
reflect the changes:
// Make the physical screen match the virtual screen. Changes made to the
// virtual screen (i.e. most other calls) will not be visible until after a
// successful call to notcurses_render().
int notcurses_render(struct notcurses* nc);
Input
Input can currently be taken only from stdin
, but on the plus side, stdin
needn't be a terminal device (unlike the ttyfp FILE*
passed in a
notcurses_options
). Generalized input ought happen soon. There is only one
input queue per struct notcurses
.
Like NCURSES, notcurses will watch for escape sequences, check them against the terminfo database, and return them as special keys. Unlike NCURSES, the fundamental unit of input is the UTF8-encoded Unicode codepoint.
// All input is currently taken from stdin, though this will likely change. We
// attempt to read a single UTF8-encoded Unicode codepoint, *not* an entire
// Extended Grapheme Cluster (despite use of the cell object, which encodes an
// entire EGC). It is also possible that we will read a special keypress, i.e.
// anything that doesn't correspond to a Unicode codepoint (e.g. arrow keys,
// function keys, screen resize events, etc.). On return, 'special' is a valid
// special key if and only if c.gcluster is 0 AND the return value is positive.
//
// Many special keys arrive as an escape sequence. It can thus be necessary for
// notcurses_getc() to wait a short time following receipt of an escape. If no
// further input is received, it is assumed that the actual Escape key was
// pressed. Otherwise, the input will be checked against the terminfo database
// to see if it indicates a special key. In all other cases, notcurses_getc()
// is non-blocking. notcurses_getc_blocking() blocks until a codepoint or
// special key is read (though it can be interrupted by a signal).
//
// In the case of a valid read, a positive value is returned corresponding to
// the number of bytes in the UTF-8 character, or '1' for all specials keys.
// 0 is returned only by notcurses_getc(), to indicate that no input was
// available. Otherwise (including on EOF) -1 is returned.
enum {
NCKEY_RESIZE,
NCKEY_UP,
NCKEY_RIGHT,
NCKEY_DOWN,
NCKEY_LEFT,
// FIXME...
} ncspecial_keys;
API int notcurses_getc(const struct notcurses* n, cell* c, int* special);
API int notcurses_getc_blocking(const struct notcurses* n, cell* c, int* special);
Planes
Fundamental to notcurses is a z-buffer of rectilinear virtual screens, known
as ncplane
s. An ncplane
can be larger than the physical screen, or smaller,
or the same size; it can be entirely contained within the physical screen, or
overlap in part, or lie wholly beyond the boundaries, never to be rendered.
Each ncplane
has a current writing state (cursor position, foreground and
background color, etc.), a backing array of UTF-8 EGCs, and a z-index. If
opaque, a cell on a higher ncplane
completely obstructs a corresponding cell
from a lower ncplane
from being seen. An ncplane
corresponds loosely to an
NCURSES Panel,
but is the primary drawing surface of notcurses—there is no object
corresponding to a bare NCURSES WINDOW
.
Cells
Unlike the notcurses
or ncplane
objects, the definition of cell
is
available to the user:
// A cell corresponds to a single character cell on some plane, which can be
// occupied by a single grapheme cluster (some root spacing glyph, along with
// possible combining characters, which might span multiple columns). At any
// cell, we can have a theoretically arbitrarily long UTF-8 string, a foreground
// color, a background color, and an attribute set. Valid grapheme cluster
// contents include:
//
// * A NUL terminator,
// * A single control character, followed by a NUL terminator,
// * At most one spacing character, followed by zero or more nonspacing
// characters, followed by a NUL terminator.
//
// Multi-column characters can only have a single style/color throughout.
//
// Each cell occupies 16 static bytes (128 bits). The surface is thus ~1.6MB
// for a (pretty large) 500x200 terminal. At 80x43, it's less than 64KB.
// Dynamic requirements can add up to 16MB to an ncplane, but such large pools
// are unlikely in common use.
typedef struct cell {
// These 32 bits are either a single-byte, single-character grapheme cluster
// (values 0--0x7f), or a pointer into a per-ncplane attached pool of
// varying-length UTF-8 grapheme clusters. This pool may thus be up to 16MB.
uint32_t gcluster; // 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;
A cell
ought be initialized with CELL_TRIVIAL_INITIALIZER
or the
cell_init()
function before it is further used. These just zero out the
cell
. A cell
has three fundamental elements:
- The EGC displayed at this coordinate, encoded in UTF-8. If the EGC is a
single ASCII character (value less than 0x80), it is stored inline in
the
cell
'sgcluster
field. Otherwise,gcluster
's top 24 bits are a 128-biased offset into the associatedncplane
's egcpool. This implies thatcell
s are associated withncplane
s once prepared. - The Curses-style attributes of the text, and a 16-bit alpha channel.
- The 48 bits of foreground and background RGB, plus a few flags.
The EGC should be loaded using cell_load()
. Either a single NUL-terminated
EGC can be provided, or a string composed of multiple EGCs. In the latter case,
the first EGC from the string is loaded. Remember, backing storage for the EGC
is provided by the ncplane
passed to cell_load()
; if this ncplane
is
destroyed (or even erased), the cell
cannot safely be used. If you're done
using the cell
before being done with the ncplane
, call cell_release()
to free up the EGC resources.
#define CELL_TRIVIAL_INITIALIZER { .gcluster = '\0', .attrword = 0, .channels = 0, }
static inline void
cell_init(cell* c){
memset(c, 0, sizeof(*c));
}
// Breaks the UTF-8 string in 'gcluster' down, setting up the cell 'c'. Returns
// the number of bytes copied out of 'gcluster', or -1 on failure. The styling
// of the cell is left untouched, but any resources are released.
int cell_load(struct ncplane* n, cell* c, const char* gcluster);
// Release resources held by the cell 'c'.
void cell_release(struct ncplane* n, cell* c);
#define CELL_STYLE_MASK 0xffff0000ul
#define CELL_ALPHA_MASK 0x0000fffful
// Set the specified style bits for the cell 'c', whether they're actively
// supported or not.
static inline void
cell_set_style(cell* c, unsigned stylebits){
c->attrword = (c->attrword & ~CELL_STYLE_MASK) |
((stylebits & 0xffff) << 16u);
}
// Add the specified styles to the cell's existing spec.
static inline void
cell_enable_styles(cell* c, unsigned stylebits){
c->attrword |= ((stylebits & 0xffff) << 16u);
}
// Remove the specified styles from the cell's existing spec.
static inline void
cell_disable_styles(cell* c, unsigned stylebits){
c->attrword &= ~((stylebits & 0xffff) << 16u);
}
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 & 0x0000000000ffffffull);
}
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);
}
#define CELL_FGDEFAULT_MASK 0x4000000000000000ull
#define CELL_BGDEFAULT_MASK 0x0000000040000000ull
static inline void
cell_rgb_set_fg(uint64_t* channels, unsigned r, unsigned g, unsigned b){
uint64_t rgb = (r & 0xffull) << 48u;
rgb |= (g & 0xffull) << 40u;
rgb |= (b & 0xffull) << 32u;
rgb |= CELL_FGDEFAULT_MASK;
*channels = (*channels & ~0x40ffffff00000000ull) | rgb;
}
static inline void
cell_rgb_set_bg(uint64_t* channels, unsigned r, unsigned g, unsigned b){
uint64_t rgb = (r & 0xffull) << 16u;
rgb |= (g & 0xffull) << 8u;
rgb |= (b & 0xffull);
rgb |= CELL_BGDEFAULT_MASK;
*channels = (*channels & ~0x0000000040ffffffull) | rgb;
}
static inline void
cell_set_fg(cell* c, unsigned r, unsigned g, unsigned b){
cell_rgb_set_fg(&c->channels, r, g, b);
}
static inline void
cell_set_bg(cell* c, unsigned r, unsigned g, unsigned b){
cell_rgb_set_bg(&c->channels, r, g, b);
}
static inline void
cell_get_fg(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));
}
static inline void
cell_get_bg(const cell* c, unsigned* r, unsigned* g, unsigned* b){
*r = cell_rgb_red(cell_bg_rgb(c->channels));
*g = cell_rgb_green(cell_bg_rgb(c->channels));
*b = cell_rgb_blue(cell_bg_rgb(c->channels));
}
static inline bool
cell_fg_default_p(const cell* c){
return !(c->channels & CELL_FGDEFAULT_MASK);
}
static inline bool
cell_bg_default_p(const cell* c){
return !(c->channels & CELL_BGDEFAULT_MASK);
}
Included tools
Four binaries are built as part of notcurses:
notcurses-demo
: some demonstration codenotcurses-view
: renders visual media (images/videos)notcurses-tester
: unit testingnotcurses-input
: decode and print keypresses
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 toupdate_panels()
. - Scrolling is disabled by default, and cannot be globally enabled.
- The Curses
cchar_t
has a fixed-size array ofwchar_t
. The notcursescell
instead supports a UTF-8 encoded extended grapheme cluster of arbitrary length. The only supported charsets areC
andUTF-8
. notcurses does not generally make use ofwchar_t
. - 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 thenewpad()
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, UTF-8, 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.).ncplane_putc()
followed bynotcurses_render()
ought be just as fast asechochar()
. - 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 asbpftrace
. - There is no timeout functionality for input (
timeout()
,halfdelay()
, etc.). Roll your own with any of the four thousand ways to do it.
Useful links
- BiDi in Terminal Emulators
- The Xterm FAQ
- The NCURSES FAQ
- ECMA-35 Character Code Structure and Extension Techniques (ISO/IEC 2022)
- ECMA-43 8-bit Coded Character Set Structure and Rules
- ECMA-48 Control Functions for Coded Character Sets (ISO/IEC 6429)
- Unicode 12.1 Full Emoji List
- Unicode Standard Annex #29 Text Segmenation
- Unicode Standard Annex #15 Normalization Forms