Remove libreadline support, implement low-level ncdirect_readline #2211 (#2212)

It was realized that our libreadline wrapper was incompatible with the new input method, indeed fundamentally so. Rip out all libreadline support. Implement a minimal ncdirect_readline() -- quite minimal, but enough to get by. We'll want to fill this out later.

So no ABI/API breakage, though perhaps some visible behavioral change.
pull/2215/head
nick black 3 years ago committed by GitHub
parent 3ddbb3c2ec
commit 436f24c770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -28,7 +28,6 @@ option(USE_GPM "Enable libgpm console mouse support" OFF)
option(USE_PANDOC "Build man pages and HTML reference with pandoc" ON)
option(USE_POC "Build small, uninstalled proof-of-concept binaries" ON)
option(USE_QRCODEGEN "Enable libqrcodegen QR code support" OFF)
option(USE_READLINE "Enable libreadline line-editing support" ON)
option(USE_STATIC "Build static libraries (in addition to shared)" ON)
set(USE_MULTIMEDIA "ffmpeg" CACHE STRING "Multimedia engine, one of 'ffmpeg', 'oiio', or 'none'")
set_property(CACHE USE_MULTIMEDIA PROPERTY STRINGS ffmpeg oiio none)
@ -74,7 +73,7 @@ elseif(APPLE)
# surely there's a better way to do this? alas, seems necessary to pull the
# pkg-config files out of Homebrew.
if(NOT DEFINED ENV{PKG_CONFIG_PATH})
set(ENV{PKG_CONFIG_PATH} "/usr/local/opt/ncurses/lib/pkgconfig:/usr/local/opt/readline/lib/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "/usr/local/opt/ncurses/lib/pkgconfig")
endif()
set(PKGCONFIG_DIR "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
else()
@ -87,11 +86,6 @@ pkg_search_module(TERMINFO REQUIRED tinfo>=6.1 ncursesw>=6.1)
set_property(GLOBAL APPEND PROPERTY PACKAGES_FOUND terminfo)
set_package_properties(terminfo PROPERTIES TYPE REQUIRED)
set(PKGCONF_REQ_PRIV "${TERMINFO_LIBRARIES}")
if(${USE_READLINE})
pkg_search_module(READLINE REQUIRED readline>=8.0)
set_property(GLOBAL APPEND PROPERTY PACKAGES_FOUND readline)
set_package_properties(readline PROPERTIES TYPE REQUIRED)
endif()
if(${USE_FFMPEG})
pkg_check_modules(AVCODEC REQUIRED libavcodec>=57.0)
pkg_check_modules(AVFORMAT REQUIRED libavformat>=57.0)
@ -307,24 +301,6 @@ target_link_libraries(notcurses-static
PUBLIC
notcurses-core-static
)
if(${USE_READLINE})
target_link_libraries(notcurses-core
PUBLIC
"${READLINE_LIBRARIES}"
)
target_link_directories(notcurses-core
PUBLIC
"${READLINE_LIBRARY_DIRS}"
)
target_link_libraries(notcurses-core-static
PUBLIC
"${READLINE_LIBRARIES}"
)
target_link_directories(notcurses-core-static
PUBLIC
"${READLINE_LIBRARY_DIRS}"
)
endif()
if(${USE_FFMPEG})
target_include_directories(notcurses
PRIVATE

@ -12,7 +12,7 @@ prepackaged on many distributions. Otherwise, acquire the current source via
Install build dependencies:
`apt-get install build-essential cmake doctest-dev zlib1g-dev libavformat-dev libavutil-dev libgpm-dev libncurses-dev libreadline-dev libqrcodegen-dev libswscale-dev libunistring-dev pandoc pkg-config`
`apt-get install build-essential cmake doctest-dev zlib1g-dev libavformat-dev libavutil-dev libgpm-dev libncurses-dev libqrcodegen-dev libswscale-dev libunistring-dev pandoc pkg-config`
If you only intend to build core Notcurses (without multimedia support), you
can omit `libavformat-dev`, `libavutil-dev`, and `libswscale-dev` from this
@ -22,15 +22,11 @@ If you want to build the Python wrappers, you'll also need:
`apt-get install python3-cffi python3-dev python3-pypandoc python3-setuptools`
If you want to build the Rust wrappers, you'll also need:
`apt-get install cargo bindgen`
### RPM
Install build dependencies:
`dnf install cmake doctest-devel zlib-devel ncurses-devel gpm-devel readline-devel libqrcodegen-devel libunistring-devel OpenImageIO-devel pandoc`
`dnf install cmake doctest-devel zlib-devel ncurses-devel gpm-devel libqrcodegen-devel libunistring-devel OpenImageIO-devel pandoc`
If you only intend to build core Notcurses (without multimedia support), you
can omit `OpenImageIO-devel`. If you're building outside Fedora Core (e.g. with
@ -42,11 +38,10 @@ Currently, building on Windows requires [MSYS2](https://www.msys2.org/) in its
64-bit Universal C Runtime (UCRT) incarnation. This builds native Windows DLLs
and EXEs, though it does not use Visual Studio. Install build dependencies:
`pacman -S mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-doctest mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-libunistring mingw-w64-ucrt-x86_64-ncurses mingw-w64-ucrt-x86_64-rust mingw-w64-ucrt-x86_64-toolchain`
`pacman -S mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-doctest mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-libunistring mingw-w64-ucrt-x86_64-ncurses mingw-w64-ucrt-x86_64-toolchain`
If you only intend to build core Notcurses (without multimedia support), you
can omit `mingw-w64-ucrt-x86_64-ffmpeg`. The Windows build never uses GNU
Readline, so don't worry about that.
can omit `mingw-w64-ucrt-x86_64-ffmpeg`.
## Building
@ -80,8 +75,8 @@ be found on the `notcurses-demo(1)` man page.
Install with `make install` following a successful build. This installs the C
core library, the C headers, the C++ library, and the C++ headers (note that
the C headers are C++-safe). It does not install the Python or Rust wrappers.
To install the Python wrappers (after installing the core library), run:
the C headers are C++-safe). It does not install the Python wrappers. To
install the Python wrappers (after installing the core library), run:
```
cd cffi
@ -89,16 +84,7 @@ python setup.py build
python setup.py install
```
The Python wrappers are also available from [PyPi](https://pypi.org/project/notcurses/). To install the low-level Rust
wrappers (`libnotcurses-sys`), run:
```
cd rust
cargo build
cargo install
```
The Rust wrappers are also available from [crates.io](https://crates.io/crates/libnotcurses-sys/).
The Python wrappers are also available from [PyPi](https://pypi.org/project/notcurses/).
### Build options
@ -117,5 +103,4 @@ but must be `Debug` for use of `USE_COVERAGE`.
* `USE_PANDOC`: build man pages with pandoc
* `USE_POC`: build small, uninstalled proof-of-concept binaries
* `USE_QRCODEGEN`: build qrcode support via libqrcodegen
* `USE_READLINE`: build readline support for Direct Mode
* `USE_STATIC`: build static libraries (in addition to shared ones)

@ -1,6 +1,16 @@
This document attempts to list user-visible changes and any major internal
rearrangements of Notcurses.
* 2.4.4 (not yet released)
* Notcurses no longer uses libreadline, as it was realized to be incompatible
with the new input system. `ncdirect_readline()` has been rewritten to
work without libreadline, which means it's now always available (readline
was an optional dependency). `NCDIRECT_OPTION_INHIBIT_CBREAK` should *not*
be used with `ncdirect_readline()`, or else it can't implement line-editing
keybindings.
* Helper function `ncwcsrtombs()` is now available for converting a
`wchar_t *` to a heap-allocated UTF-8 `char *`.
* 2.4.3 (2021-09-26)
* `ncplane_erase_region()` has been made much more general, and can now
operate relative to the current cursor.

@ -111,7 +111,6 @@ may well be possible to use still older versions. Let me know of any successes!
* (build+runtime) From [NCURSES](https://invisible-island.net/ncurses/announce.html): terminfo 6.1+
* (build+runtime) GNU [libunistring](https://www.gnu.org/software/libunistring/) 0.9.10+
* (OPTIONAL) (build+runtime) [libgpm](https://www.nico.schottelius.org/software/gpm/) 1.20+
* (OPTIONAL) (build+runtime) GNU [Readline](https://www.gnu.org/software/readline/) 8.0+
* (OPTIONAL) (build+runtime) From QR-Code-generator: [libqrcodegen](https://github.com/nayuki/QR-Code-generator) 1.5.0+
* (OPTIONAL) (build+runtime) From [FFmpeg](https://www.ffmpeg.org/): libswscale 5.0+, libavformat 57.0+, libavutil 56.0+
* (OPTIONAL) (build+runtime) [OpenImageIO](https://github.com/OpenImageIO/oiio) 2.15.0+, requires C++
@ -248,15 +247,22 @@ If things break or seem otherwise lackluster, **please** consult the
<code>xterm</code>!
</details>
<details>
<summary>Can I write a CLI program (scrolling, fits in with the shell, etc.)
with Notcurses?</summary>
Yes! Use the flags <code>NCOPTION_NO_ALTERNATE_SCREEN</code>,
<code>NCOPTION_NO_CLEAR_BITMAPS</code>, and <code>NCOPTION_PRESERVE_CURSOR</code>,
and call `ncplane_set_scrolling()` on the standard plane. You still must
explicitly render.
<details>
<summary>Can I have Notcurses without this huge multimedia stack?</summary>
Yes! Build with <code>-DUSE_MULTIMEDIA=none</code>.
Again yes! Build with <code>-DUSE_MULTIMEDIA=none</code>.
</details>
<details>
<summary>Can I build this individual Notcurses program without aforementioned
multimedia stack?</summary>
Again yes! Use <code>notcurses_core_init()</code> or
Almost unbelievably, yes! Use <code>notcurses_core_init()</code> or
<code>ncdirect_core_init()</code> in place of <code>notcurses_init()</code>/
<code>ncdirect_init()</code>, and link with <code>-lnotcurses-core</code>.
Your application will likely start a few milliseconds faster;
@ -302,6 +308,7 @@ If things break or seem otherwise lackluster, **please** consult the
language settings", click "Change system locale", and check the "Beta: Use
Unicode UTF-8 for worldwide language support" option. Restart the computer.
That ought help a little bit. Ensure your code page is 65001 with `chcp 65001`.
Try playing with fonts.
</details>
<details>
@ -403,13 +410,31 @@ If things break or seem otherwise lackluster, **please** consult the
<details>
<summary>How can I use Direct Mode in conjunction with libreadline?</summary>
Pass <code>NCDIRECT_OPTION_CBREAK</code> to <code>ncdirect_init()</code>, and
ensure you do not pass <code>NCDIRECT_OPTION_NO_READLINE</code>. If you'd like,
set <code>rl_readline_name</code> and <code>rl_attempted_completion_function</code>
prior to calling <code>ncdirect_init()</code>. With that said, consider using
a Notcurses <code>ncreader</code>.
You can't anymore (you could up until 2.4.1, but the new input system is
fundamentally incompatible with it). `ncdirect_readline()` still exists,
though, and now actually works even without libreadline, though it is of
course not exactly libreadline. In any case, you'd probably be better off
using CLI mode with a <code>ncreader</code>.
</details>
<details>
<summary>So is Direct Mode deprecated or what?</summary>
It is not currently deprecated, and definitely receives bugfixes. You are
probably better served using CLI mode (see above), which came about
somewhat late in Notcurses development (the 2.3.x series), but is superior
to Direct Mode in pretty much every way. The only reason to use Direct
Mode is if you're going to have other programs junking up your display.
</details>
<details>
<summary>Direct Mode sounds fast! Since it's, like, direct.</summary>
Direct mode is <i>substantially slower</i> than rendered mode. Rendered
mode assumes it knows what's on the screen, and uses this information to
generate optimized sequences of escapes and glyphs. Direct mode writes
everything it's told to write. It is furthermore far less capable—all
widgets etc. are available only to rendered mode, and will definitely
not be extended to Direct Mode.
<details>
<summary>Will there ever be Java wrappers?</summary>
I should hope not. If you want a Java solution, try Autumn Lamonte's

@ -414,10 +414,9 @@ This context must be destroyed using `ncdirect_stop()`. The following functions
are available for direct mode:
```c
// Read a (heap-allocated) line of text using the Readline library Initializes
// Readline the first time it's called. For input to be echoed to the terminal,
// it is necessary that NCDIRECT_OPTION_INHIBIT_CBREAK be provided to
// ncdirect_init(). Returns NULL on error.
// Read a (heap-allocated) newline-delimited chunk of text. Returns NULL on
// failure. The NCDIRECT_OPTION_INHIBIT_CBREAK flag ought not be used together
// with this function, or the line-editing keybindings cannot be honored.
API char* ncdirect_readline(struct ncdirect* nc, const char* prompt);
int ncdirect_fg_rgb(struct ncdirect* nc, unsigned rgb);
@ -2660,7 +2659,7 @@ xxxxxxxxxxxxxxxx╰─────────────╯xxxxxxxxxxxxxxxxxxx
### Readers
ncreaders provide freeform input in a (possibly multiline) region, supporting
optional readline keybindings.
optional libreadline-style keybindings.
```c
typedef struct ncreader_options {

@ -209,11 +209,11 @@ output stream, taking effect immediately.
Attempting to e.g. move up while on the top row will return 0, but have no
effect.
**ncdirect_readline** uses the Readline library to read a (heap-allocated)
line of arbitrary length, supporting line-editing controls. For more
information, consult **readline(3)**. If you want input echoed to the
terminal while using **ncdirect_readline**, **NCDIRECT_OPTION_INHIBIT_CBREAK**
must be supplied to **ncdirect_init**.
**ncdirect_readline** reads a (heap-allocated) line of arbitrary length,
supporting some libreadline-style line-editing controls.
**NCDIRECT_OPTION_INHIBIT_CBREAK** should not be used if you intend to
use **ncdirect_readline**; if used, line-editing keybindings cannot be
implemented. Input will be echoed whether this option is used or not.
**ncdirect_check_pixel_support** must be called (and successfully return)
before **NCBLIT_PIXEL** can be used to render images; see
@ -253,7 +253,6 @@ mapping them to **NCDIRECT_OPTION_VERBOSE** and
# SEE ALSO
**getenv(3)**,
**readline(3)**
**notcurses(3)**,
**notcurses_plane(3)**,
**notcurses_visual(3)**,

@ -54,10 +54,8 @@ API ALLOC struct ncdirect* ncdirect_init(const char* termtype, FILE* fp, uint64_
// allowing for a svelter binary. Link with notcurses-core if this is used.
API ALLOC struct ncdirect* ncdirect_core_init(const char* termtype, FILE* fp, uint64_t flags);
// Read a (heap-allocated) line of text using the Readline library Initializes
// Readline the first time it's called. For input to be echoed to the terminal,
// it is necessary that NCDIRECT_OPTION_INHIBIT_CBREAK be provided to
// ncdirect_init(). Returns NULL on error.
// Read a (heap-allocated) newline-delimited chunk of text, after printing the
// prompt. The newline itself, if present, is included. Returns NULL on error.
__attribute__ ((nonnull (1)))
API ALLOC char* ncdirect_readline(struct ncdirect* nc, const char* prompt);

@ -1837,24 +1837,33 @@ API int ncplane_putegc_stained(struct ncplane* n, const char* gclust, int* sbyte
// 0x0--0x10ffff can be UTF-8-encoded with only 4 bytes
#define WCHAR_MAX_UTF8BYTES 4
// ncplane_putegc(), but following a conversion from wchar_t to UTF-8 multibyte.
static inline int
ncplane_putwegc(struct ncplane* n, const wchar_t* gclust, int* sbytes){
// generate a heap-allocated UTF-8 encoding of the wide string 'src'.
ALLOC static inline char*
ncwcsrtombs(const wchar_t* src){
mbstate_t ps;
memset(&ps, 0, sizeof(ps));
const wchar_t** wset = &gclust;
size_t mbytes = wcsrtombs(NULL, wset, 0, &ps);
size_t mbytes = wcsrtombs(NULL, &src, 0, &ps);
if(mbytes == (size_t)-1){
return -1;
return NULL;
}
++mbytes;
char* mbstr = (char*)malloc(mbytes); // need cast for c++ callers
if(mbstr == NULL){
return -1;
return NULL;
}
size_t s = wcsrtombs(mbstr, wset, mbytes, &ps);
size_t s = wcsrtombs(mbstr, &src, mbytes, &ps);
if(s == (size_t)-1){
free(mbstr);
return NULL;
}
return mbstr;
}
// ncplane_putegc(), but following a conversion from wchar_t to UTF-8 multibyte.
static inline int
ncplane_putwegc(struct ncplane* n, const wchar_t* gclust, int* sbytes){
char* mbstr = ncwcsrtombs(gclust);
if(mbstr == NULL){
return -1;
}
int ret = ncplane_putegc(n, mbstr, sbytes);

@ -9,9 +9,6 @@
#include "visual-details.h"
#include "notcurses/direct.h"
#include "internal.h"
#ifdef USE_READLINE
#include <readline/readline.h>
#endif
// conform to the foreground and background channels of 'channels'
static int
@ -192,6 +189,13 @@ cursor_yx_get(ncdirect* n, int ttyfd, const char* u7, int* y, int* x){
if(tty_emit(u7, ttyfd)){
return -1;
}
int fakey, fakex;
if(y == NULL){
y = &fakey;
}
if(x == NULL){
x = &fakex;
}
get_cursor_location(ictx, y, x);
loginfo("cursor at y=%d x=%d\n", *y, *x);
return 0;
@ -834,11 +838,6 @@ static int
ncdirect_stop_minimal(void* vnc){
ncdirect* nc = vnc;
int ret = drop_signals(nc);
if(nc->initialized_readline){
#ifdef USE_READLINE
rl_deprep_terminal();
#endif
}
fbuf f = {};
if(fbuf_init_small(&f) == 0){
ret |= reset_term_attributes(&nc->tcache, &f);
@ -945,21 +944,103 @@ int ncdirect_stop(ncdirect* nc){
return ret;
}
// our new input system is fundamentally incompatible with libreadline, so we
// have to fake it ourselves. at least it saves us the dependency.
//
// if NCDIRECT_OPTION_INHIBIT_CBREAK is in play, we're not going to get the
// text until cooked mode has had its way with it, and we are essentially
// unable to do anything clever. text will be echoed, and there will be no
// line-editing keybindings, save any implemented in the line discipline.
//
// otherwise, we control echo. whenever we emit output, get our position. if
// we've changed line, assume the prompt has scrolled up, and account for
// that. we return to the prompt, clear any affected lines, and reprint what
// we have.
char* ncdirect_readline(ncdirect* n, const char* prompt){
#ifdef USE_READLINE
if(!n->initialized_readline){
rl_outstream = n->ttyfp;
rl_instream = stdin;
rl_prep_terminal(1); // 1 == read 8-bit input
n->initialized_readline = true;
}
return readline(prompt);
#else
logerror("notcurses was built without readline support\n");
(void)n;
(void)prompt;
const char* u7 = get_escape(&n->tcache, ESCAPE_U7);
if(!u7){ // we probably *can*, but it would be a pita; screw it
logerror("can't readline without u7\n");
return NULL;
}
if(fprintf(n->ttyfp, "%s", prompt) < 0){
return NULL;
}
// FIXME what if we're reading from redirected input, not a terminal?
int y, xstart;
if(cursor_yx_get(n, n->tcache.ttyfd, u7, &y, &xstart)){
return NULL;
}
int tline = y;
int bline = y;
wchar_t* str;
int wspace = BUFSIZ / sizeof(*str);
if((str = malloc(wspace * sizeof(*str))) == NULL){
return NULL;
}
int wused = 0; // number used
str[wused++] = L'\0';
ncinput ni;
uint32_t id;
int oldx = xstart;
while((id = ncdirect_getc_blocking(n, &ni)) != (uint32_t)-1){
if(id == NCKEY_ENTER){
if(fputc('\n', n->ttyfp) < 0){
free(str);
return NULL;
}
char* ustr = ncwcsrtombs(str);
return ustr;
}else if(id == NCKEY_BACKSPACE){
if(wused > 1){
str[wused - 2] = L'\0';
--wused;
}
}else{
if(wspace - 1 < wused){
wspace += BUFSIZ;
wchar_t* tmp = realloc(str, wspace * sizeof(*str));
if(tmp == NULL){
free(str);
return NULL;
}
str = tmp;
}
str[wused - 1] = id;
++wused;
str[wused - 1] = L'\0';
// FIXME check modifiers
int x;
if(cursor_yx_get(n, n->tcache.ttyfd, u7, &y, &x)){
break;
}
if(x < oldx){
oldx = x;
if(--tline < 0){
tline = 0;
}
}
if(y > bline){
bline = y;
}
}
const char* el = get_escape(&n->tcache, ESCAPE_EL);
for(int i = bline ; i >= tline ; --i){
if(ncdirect_cursor_move_yx(n, i, i > tline ? 0 : xstart)){
break;
}
if(term_emit(el, n->ttyfp, false)){
break;
}
}
if(fprintf(n->ttyfp, "%ls", str) < 0){
break;
}
if(fflush(n->ttyfp)){
break;
}
}
free(str);
return NULL;
#endif
}
static inline int

@ -287,7 +287,6 @@ typedef struct ncdirect {
tinfo tcache; // terminfo cache
uint64_t channels; // current channels
uint16_t stylemask; // current styles
bool initialized_readline; // have we initialized Readline?
uint64_t flags; // copied in ncdirect_init() from param
ncsharedstats stats; // stats! not as broadly used as in notcurses
} ncdirect;

@ -5,15 +5,30 @@
static int
rl(struct ncdirect* n){
char* l;
while( (l = ncdirect_readline(n, "ncdirect")) ){
while( (l = ncdirect_readline(n, "[ncdirect] ")) ){
fprintf(stderr, "input: [%s]\n", l);
free(l);
}
return 0;
}
int main(void){
uint64_t flags = NCDIRECT_OPTION_INHIBIT_CBREAK;
static void
usage(void){
fprintf(stderr, "usage: readline [ -v ]\n");
exit(EXIT_FAILURE);
}
int main(int argc, char** argv){
uint64_t flags = 0;
if(argc != 1){
if(argc != 2){
usage();
}
if(strcmp(argv[1], "-v")){
usage();
}
flags = NCDIRECT_OPTION_VERY_VERBOSE;
}
struct ncdirect* n = ncdirect_core_init(NULL, NULL, flags);
if(n == NULL){
return EXIT_FAILURE;

Loading…
Cancel
Save