Some notes on computer stuff

ncurses wrappers for C++

April 24, 2016
[curses] [c++] [programming]

curses is a well known set of libraries (with mostly common API) for creating TUIs. What is significantly less known though is that ncurses implementation comes with C++ wrappers for its C API.

The wrappers seem to not see an update for many years, but they still work and are quite usable. The obvious benefits over usual C API are more object-oriented interface, taking advantage of function grouping via classes, more strict type checking, function overloading and default values of arguments. The downsides are lack of documentation (although there are comments in headers and a demo project demo.cc in sources) and some implementation flaws.

So I found these wrappers wanting to do some quick prototyping of TUI and decided to try using them. Lack of documentation and above mentioned flaws made this harder than it should be, hence this post which tries to give a few pointers on how to and how not to use some of these wrappers. The description here is nowhere near complete and touches only on several topics I struggled with.

NCursesApplication

Using predefined main()

No need to write main() if you setup creation of an object derived from NCursesApplication somewhere in your code. For example:

#include <cursesapp.h>

class App : public NCursesApplication
{
    // ...
};

static App app;

Or writing your own main()

In practice however you might prefer to write main() anyway as the one provided by ncurses++ catches exceptions by reference (correct thing to do), but at least NCursesMenuItem throws pointers (wrong thing to do). Example:

int main(int argc, char *argv[])
{
    setlocale(LC_ALL, "");

    int res;

    try {
        App app;
        app.handleArgs(argc, argv);
        res = app();
        endwin();
    } catch (const NCursesException *e) {
        endwin();
        std::cerr << e->message << std::endl;
        res = e->errorno;
    } catch (const NCursesException &e) {
        endwin();
        std::cerr << e.message << std::endl;
        res = e.errorno;
    } catch (const std::exception &e) {
        endwin();
        std::cerr << "Exception: " << e.what() << std::endl;
        res = EXIT_FAILURE;
    }
    return res;
}

Handling arguments

Arguments can be handled by overriding handleArgs() method of NCursesApplication:

virtual void handleArgs(int argc, char* argv[]) {
    // ...
}

Setting up title

Title either appears always or never, so size should be constant and non-zero if title is needed, otherwise corresponding window just isn't created inside NCursesApplication.

Menus

Construction and item ownership

Menus can be constructed either by passing them list of items (which are pointers to objects of types derived from NCursesMenuItem) via constructors or by invoking different special constructor and using InitMenu() function afterward.

NCursesMenu class supports two item management schemes:

  1. Lifetimes of the list of items and items are managed by the client.
  2. The list of items along with items are transfered to NCursesMenu class.

The scheme is picked via autoDelete_Items parameter of constructor/InitMenu(). Beware that menu items should be detached from menu before they can be destroyed, this means that when not using heap special care must be taken to delete NCursesMenu before deleting any of its items. If you don't do that, the program is likely to be terminated because of nested exception thrown from item destructors (yeah, they do this kind of thing...).

Example below shows how to work around this via Base-from-Member idiom.

Constraints on list of menu items

No duplicates. List of items shouldn't contain duplicates or weird effects will arise. The library will use same low-level item for both items (plus leaking N - 1 if there are N duplicates), which won't do any good.

Terminating null-element. List should be terminated with a pointer to an empty NCursesMenuItem object (the one constructed by calling NCursesMenuItem()). Such empty menu items can be removed freely as they don't own resources.

Not empty. There must be at least one non-null item or exception is thrown.

Customizing controls

Special menu keys can be customized by overwriting virtual function NCursesMenu::virtualize(), which should return codes suitable for passing into menu_driver(). For example, j and k keys can be configured to move up and down by one item, just like in Vim:

virtual int virtualize(int c)
{
    switch (c) {
        case 'j': return REQ_DOWN_ITEM;
        case 'k': return REQ_UP_ITEM;

        default: return NCursesMenu::virtualize(c);
    }
}

Menu size

Menu doesn't fill all available space automatically, use set_format() method. E.g.:

set_format(lines() - 1, 1);

Broken setItems()

setItems() doesn't seem to work in all cases. It always tries to free memory, even the one it doesn't own. So if you thought that you could use it to work around issue with destroying items before the menu, the answer is no, you can't.

Multiple selection

First, it should be enabled:

options_off(O_ONEVALUE); // disable single-selection mode
set_mark("*");           // setup selected item marker (not directory related)

Second, whether each item is selected or not can be determined via NCursesMenuItem::value() method:

if (item->value()) {
    // ...
}

Menu item strings ownership

Just as new_item() function, NCursesMenuItem doesn't copy its arguments! This means that one shouldn't pass in temporary strings or strings that won't outlive the menu item. Better way of ensuring this is to keep a copy in std::string and bound its lifetime to NCursesMenuItem object.

Panels

If you want to display something and keep it there, use panels (NCursesPanel) and not just windows (NCursesWindow; panels are windows with additional layering functionality), which will be automatically updated as needed.

Accessing stdscr

To access state of the screen use one of these:

  1. NCursesPanel screen;
  2. NCursesWindow screen(::stdscr)

and then do screen.cols(), screen.lines(), etc.

Linking against the wrappers

Linking with ncurses++ requires the following libraries (even if you don't use, say, forms out of this list):

-lncurses++w -lformw -lmenuw -lpanelw -lncursesw

Integrating readline library into the application

Thanks to flexibility of readline library, it's easy to plug it into curses application. Here's the code (no header/source separation for brevity):

// Compilation: g++ cmdline.cpp -c -std=c++11

#include <cursesp.h>

#include <readline/history.h>
#include <readline/readline.h>

#include <cstddef>
#include <cstdlib>

#include <memory>
#include <string>

static inline std::wstring to_wide(const std::string &s);

class CmdLine
{
public:
    CmdLine()
    {
        rl_redisplay_function = &readlineRedisplayThunk;
        // Prevent displaying completion menu, which could mess up output.
        rl_completion_display_matches_hook = [](char *[], int, int) {};

        NCursesWindow screen(::stdscr);
        cmdline.reset(new NCursesPanel(1, screen.cols(), screen.lines() - 1,
                                       0));
    }

    ~CmdLine()
    {
        cmdline->hide();
        NCursesWindow(::stdscr).refresh();
    }

public:
    std::string prompt(const std::string &prompt)
    {
        instance = this;

        const int visibility = curs_set(1);
        std::unique_ptr<char, decltype(&std::free)> line {
            readline(prompt.c_str()), &std::free
        };
        curs_set(visibility);

        add_history(line.get());

        return line.get();
    }

private:
    static void readlineRedisplayThunk()
    {
        instance->readlineRedisplay();
    }

private:
    void readlineRedisplay()
    {
        std::wstring prompt = to_wide(rl_display_prompt);
        std::wstring linebuf = to_wide(rl_line_buffer);

        std::size_t prompt_width = wcswidth(prompt.c_str(),
                                            static_cast<std::size_t>(-1));
        std::size_t cursor_col = prompt_width
                               + wcswidth(linebuf.c_str(), rl_point);

        cmdline->erase();
        cmdline->printw(0, 0, "%s%s", rl_display_prompt, rl_line_buffer);

        if (static_cast<int>(cursor_col) >= NCursesWindow(::stdscr).cols()) {
            // Hide the cursor if it lies outside the window. Otherwise it'll
            // appear on the very right.
            curs_set(0);
        } else {
            cmdline->move(0, cursor_col);
            curs_set(2);
        }

        cmdline->refresh();
    }

private:
    std::unique_ptr<NCursesPanel> cmdline;

private:
    // XXX: NEITHER thread safe NOR re-enterable.
    static CmdLine *instance;
};

CmdLine *CmdLine::instance;

static inline std::wstring to_wide(const std::string &s)
{
    const std::size_t len = mbstowcs(NULL, s.c_str(), 0);
    if (len == static_cast<std::size_t>(-1)) {
        return std::wstring();
    }

    std::wstring result(len + 1U, L'\0');
    static_cast<void>(mbstowcs(&result[0], s.c_str(), len + 1U));
    return result;
}

And usage example:

const std::string &input = CmdLine().prompt(":");
doSomeProcessing(input);

readline adds this to linker flags:

-lreadline -ltermcap

Example

As an exercise let's write simple filter-like tool that allows picking one or more elements from the list. It will accept list items either via arguments or from standard input.

The code above was C++11, this one is C++98. It's also bigger in size so posted elsewhere. There are comments which restate some details of using ncurses++ from above.

I didn't bother handling resizing, so it might not work.

Further information

There is almost no documentation at all, so read the sources (in source package it's under c++/ directory), README-first file there or just headers (cursesapp.h, cursesf.h, cursesm.h, cursesp.h, cursesw.h, cursslk.h).

Couldn't find any articles either, but with some patience one can figure out details of the code. It's small enough and it's a set of wrappers, so nothing sophisticated.